GODCALL fork: English UI+prompts, http Gitea fetcher, self-buildable images
- Translate panel UI (~500 strings) and all extraction/wiki prompts to English; rebrand to GODCALL (title, logo, login, header) - git-fetcher: allow http:// clone URLs behind KNOWLEDGE_ALLOW_HTTP=1 (tailnet Gitea) - MemoryCore/Dockerfile: drop-in image (upstream ships none) with TDAI_GATEWAY_CONFIG env + healthcheck; combined-hub Dockerfile: stock Debian mirrors; stub MemoryKnowledge openapi.yaml (absent upstream but COPYed) - start-memory-hub.sh: optional GIT_CREDENTIALS_FILE mount (keeps tokens out of repo URLs); tools/import-gitea.mjs: bulk CodeGraph import of all Gitea repos Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3df79326d
commit
260f02915f
31
MemoryCore/Dockerfile
Normal file
31
MemoryCore/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# GODCALL memory-core — drop-in replacement for agentmemory/memory-core.
|
||||||
|
# Matches the upstream image contract exactly:
|
||||||
|
# WORKDIR /app, ENTRYPOINT tini, CMD node --import tsx src/gateway/server.ts
|
||||||
|
# config mounted at /data/config/tdai-gateway.yaml, data volume at /data/tdai-memory
|
||||||
|
#
|
||||||
|
# Build (from repo root):
|
||||||
|
# docker build -t godcall/memory-core:local -f MemoryCore/Dockerfile MemoryCore
|
||||||
|
|
||||||
|
FROM node:22-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 make g++ git ca-certificates tini curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
TDAI_GATEWAY_CONFIG=/data/config/tdai-gateway.yaml
|
||||||
|
EXPOSE 8420
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
|
||||||
|
CMD curl -sf http://localhost:8420/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["node", "--import", "tsx", "src/gateway/server.ts"]
|
||||||
@ -13,125 +13,125 @@ import type { MemoryRecord, ExtractedMemory } from "../record/l1-writer.js";
|
|||||||
// System Prompt
|
// System Prompt
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
export const CONFLICT_DETECTION_SYSTEM_PROMPT = `你是记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。
|
export const CONFLICT_DETECTION_SYSTEM_PROMPT = `You are a memory conflict detector. Batch-compare multiple [New Memories] against the existing memories in the [Unified Candidate Memory Pool], deciding how to handle each one.
|
||||||
|
|
||||||
**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言;JSON 字段名、枚举值、record_id、ISO 时间戳保持英文。
|
**Output language**: write \`merged_content\` in the same language as the existing memories in the candidate pool; keep JSON field names, enum values, record_id, and ISO timestamps in English.
|
||||||
|
|
||||||
## 核心规则
|
## Core rules
|
||||||
|
|
||||||
- **跨 type 合并**:不同 type(persona / episodic / instruction / work_fact / work_task / work_method / work_artifact)的记忆如果语义上描述同一事实/事件,**可以合并**。
|
- **Cross-type merging**: memories of different types (persona / episodic / instruction / work_fact / work_task / work_method / work_artifact) **may be merged** if they semantically describe the same fact/event.
|
||||||
- **多对多合并**:一条新记忆可以同时替换/合并候选池中的**多条**已有记忆(通过 target_ids 数组指定)。
|
- **Many-to-many merging**: one new memory may simultaneously replace/merge **multiple** existing memories in the candidate pool (specified via the target_ids array).
|
||||||
- 合并后你必须判断新记忆的最佳 type(merged_type)。
|
- After merging, you must decide the new memory's best type (merged_type).
|
||||||
|
|
||||||
## 判断逻辑
|
## Judgment logic
|
||||||
|
|
||||||
1. **分辨记忆性质**:
|
1. **Distinguish the memory's nature**:
|
||||||
- **状态类**(persona/instruction):偏好、特质、长期设定、相对稳定的事实、行为规则
|
- **State-like** (persona/instruction): preferences, traits, long-term settings, relatively stable facts, behavior rules
|
||||||
- **事件类**(episodic):一次性经历、带时间点的客观记录,建议合并同一件事的前因后果
|
- **Event-like** (episodic): one-off experiences, objective records with a point in time; prefer merging the cause and effect of the same event
|
||||||
|
|
||||||
2. **判断是否同一事实/事件**:主体相同、主题一致、时间接近、scene_name 相似
|
2. **Decide whether it is the same fact/event**: same subject, consistent topic, close in time, similar scene_name
|
||||||
|
|
||||||
3. **选择动作**:
|
3. **Choose the action**:
|
||||||
- "store":视为新信息,新增当前记忆。
|
- "store": treat as new information; add the current memory.
|
||||||
- "skip":已有记忆更好,新记忆无增量或更模糊,忽略当前记忆。
|
- "skip": the existing memory is better — the new memory adds nothing or is vaguer; ignore the current memory.
|
||||||
- "update":同一事实/事件,新记忆在内容或时间上更优(更具体、更晚或纠错),以新记忆为主覆盖旧记忆,可保留旧记忆中仍正确的细节。
|
- "update": same fact/event, and the new memory is better in content or time (more specific, later, or a correction); overwrite the old memory with the new one as the primary source, optionally preserving still-correct details from the old memory.
|
||||||
- "merge":同一事实或同一演化过程,多条记忆信息互补且不矛盾,合并成一条更完整记忆,信息尽量不冗余。
|
- "merge": same fact or the same evolving process; multiple memories are complementary and non-contradictory — merge them into one more complete memory, keeping the information as non-redundant as possible.
|
||||||
|
|
||||||
4. **策略倾向**:
|
4. **Strategy tendencies**:
|
||||||
- 状态类:多条描述同一偏好/特质 → 倾向 merge;无增量 → skip;明确更新 → update
|
- State-like: multiple descriptions of the same preference/trait → lean merge; no new information → skip; an explicit update → update
|
||||||
- 事件类:同一事件的前因后果、不同阶段 → 倾向 merge 为一条完整叙述;完全相同 → skip
|
- Event-like: cause/effect or different phases of the same event → lean merge into one complete narrative; exact duplicate → skip
|
||||||
- 跨类型示例:一条 episodic "用户在 2018 年开始做播客" + 一条 persona "用户有播客制作经验" → 可 merge 为一条 persona 或 episodic(取决于信息侧重)
|
- Cross-type example: an episodic "The user started making a podcast in 2018" + a persona "The user has podcast-production experience" → may merge into one persona or episodic memory (depending on where the information's emphasis lies)
|
||||||
|
|
||||||
5. **timestamp 处理**:
|
5. **timestamp handling**:
|
||||||
- merge / update 时,merged_timestamps 应包含**所有相关记忆的时间戳并集**(去重排序)
|
- On merge / update, merged_timestamps should contain the **union of all related memories' timestamps** (deduplicated and sorted)
|
||||||
- 这样可以保留事件发生的完整时间线
|
- This preserves the complete timeline of when the events occurred
|
||||||
|
|
||||||
## 输出格式
|
## Output format
|
||||||
|
|
||||||
严格输出 JSON 数组,每个元素对应一条新记忆的决策。不输出任何其他内容:
|
Output strictly a JSON array, one element per new memory's decision. Output nothing else:
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"record_id": "新记忆的 record_id",
|
"record_id": "the new memory's record_id",
|
||||||
"action": "store|update|skip|merge",
|
"action": "store|update|skip|merge",
|
||||||
"target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
|
"target_ids": ["candidate memory record_id 1 to delete", "record_id 2"],
|
||||||
"merged_content": "合并/更新后的记忆内容(merge/update 时必填)",
|
"merged_content": "the merged/updated memory content (required for merge/update)",
|
||||||
"merged_type": "合并后的最佳 type:persona|episodic|instruction|work_fact|work_task|work_method|work_artifact(merge/update 时必填)",
|
"merged_type": "the best type after merging: persona|episodic|instruction|work_fact|work_task|work_method|work_artifact (required for merge/update)",
|
||||||
"merged_priority": 85,
|
"merged_priority": 85,
|
||||||
"merged_timestamps": ["合并后的时间戳数组,包含所有新旧记忆时间戳的并集(merge/update 时必填)"]
|
"merged_timestamps": ["the merged timestamp array, containing the union of all old and new memory timestamps (required for merge/update)"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
字段说明:
|
Field notes:
|
||||||
- target_ids:要删除替换的旧记忆 ID **数组**(可以 1 条或多条)。store/skip 时省略或为空。
|
- target_ids: the **array** of old memory IDs to delete and replace (1 or more allowed). Omit or leave empty for store/skip.
|
||||||
- merged_content:merge/update 时的最终记忆文本。store/skip 时省略。
|
- merged_content: the final memory text for merge/update. Omit for store/skip.
|
||||||
- merged_type:merge/update 后记忆应归属的 type。根据合并后内容本质判断。
|
- merged_type: the type the memory should belong to after merge/update. Judge from the essential nature of the merged content.
|
||||||
- merged_priority:merge/update 后的新优先级(0-100 整数,merge/update 时必填)。合并后信息更完整、更确定,通常应**酌情提升** priority(例如两条 priority 70 的记忆合并后可提升到 80)。参考标准:80-100(核心特质/重要事件),60-79(一般偏好/普通活动),<60(次要信息)。
|
- merged_priority: the new priority after merge/update (integer 0-100, required for merge/update). Since merged information is more complete and more certain, you should usually **raise** priority as appropriate (e.g. two priority-70 memories may rise to 80 after merging). Reference scale: 80-100 (core traits / important events), 60-79 (ordinary preferences / everyday activities), <60 (minor information).
|
||||||
- merged_timestamps:合并后的时间戳数组。收集新记忆 + 所有被合并旧记忆的时间戳,去重排序。`;
|
- merged_timestamps: the merged timestamp array. Collect the timestamps of the new memory + all merged-away old memories, deduplicate and sort.`;
|
||||||
|
|
||||||
export const WORK_CONFLICT_DETECTION_SYSTEM_PROMPT = `你是团队工作记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。
|
export const WORK_CONFLICT_DETECTION_SYSTEM_PROMPT = `You are a team work-memory conflict detector. Batch-compare multiple [New Memories] against the existing memories in the [Unified Candidate Memory Pool], deciding how to handle each one.
|
||||||
|
|
||||||
**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言;JSON 字段名、枚举值、record_id、ISO 时间戳保持英文。
|
**Output language**: write \`merged_content\` in the same language as the existing memories in the candidate pool; keep JSON field names, enum values, record_id, and ISO timestamps in English.
|
||||||
|
|
||||||
## 核心规则
|
## Core rules
|
||||||
|
|
||||||
- **跨 type 合并**:不同 type(work_fact / work_task / work_method / work_artifact)的记忆如果语义上描述同一工作对象、任务、方法或资产,**可以合并**。
|
- **Cross-type merging**: memories of different types (work_fact / work_task / work_method / work_artifact) **may be merged** if they semantically describe the same work object, task, method, or asset.
|
||||||
- **多对多合并**:一条新记忆可以同时替换/合并候选池中的**多条**已有记忆(通过 target_ids 数组指定)。
|
- **Many-to-many merging**: one new memory may simultaneously replace/merge **multiple** existing memories in the candidate pool (specified via the target_ids array).
|
||||||
- 合并后你必须判断新记忆的最佳 type(merged_type)。
|
- After merging, you must decide the new memory's best type (merged_type).
|
||||||
- 记忆默认会在项目团队内共享,合并内容应只保留工作相关信息。
|
- Memories are shared within the project team by default; merged content should keep only work-related information.
|
||||||
|
|
||||||
## 判断逻辑
|
## Judgment logic
|
||||||
|
|
||||||
1. **分辨记忆性质**:
|
1. **Distinguish the memory's nature**:
|
||||||
- **工作事实类(work_fact)**:项目事实、需求、决策、状态、风险、约束、实验结果、客户反馈。
|
- **Work-fact-like (work_fact)**: project facts, requirements, decisions, status, risks, constraints, experiment results, customer feedback.
|
||||||
- **工作任务类(work_task)**:待办、owner、deadline、下一步计划、任务状态变化。
|
- **Work-task-like (work_task)**: to-dos, owner, deadline, next-step plans, task status changes.
|
||||||
- **工作方法类(work_method)**:SOP、禁忌、原则、经验、设计思路、判断标准、Agent 行为规则。
|
- **Work-method-like (work_method)**: SOPs, taboos, principles, experience, design rationale, judgment criteria, Agent behavior rules.
|
||||||
- **工作资产类(work_artifact)**:文档、PR、Issue、Prompt、报告、代码分支、设计稿、链接等。
|
- **Work-artifact-like (work_artifact)**: documents, PRs, Issues, Prompts, reports, code branches, design drafts, links, etc.
|
||||||
|
|
||||||
2. **判断是否同一工作对象/演化过程**:
|
2. **Decide whether it is the same work object / evolving process**:
|
||||||
- 同一项目、模块、需求、任务、风险、决策、方法、资产,且 scene_name 或语义高度相似。
|
- Same project, module, requirement, task, risk, decision, method, or asset, with a highly similar scene_name or semantics.
|
||||||
- 同一任务的不同阶段、同一方法的补充、同一资产的版本或用途变化,通常可以合并。
|
- Different phases of the same task, additions to the same method, or version/usage changes of the same asset can usually be merged.
|
||||||
- 仅属于同一大项目但讨论对象不同,不应强行合并。
|
- Belonging to the same large project but discussing different objects should NOT be force-merged.
|
||||||
|
|
||||||
3. **选择动作**:
|
3. **Choose the action**:
|
||||||
- "store":视为新信息,新增当前记忆。
|
- "store": treat as new information; add the current memory.
|
||||||
- "skip":已有记忆更好,新记忆无增量或更模糊,忽略当前记忆。
|
- "skip": the existing memory is better — the new memory adds nothing or is vaguer; ignore the current memory.
|
||||||
- "update":同一工作对象,新记忆更具体、更新、更权威或纠正旧信息,以新记忆为主覆盖旧记忆,可保留旧记忆中仍正确的细节。
|
- "update": same work object, and the new memory is more specific, newer, more authoritative, or corrects the old information; overwrite the old memory with the new one as the primary source, optionally preserving still-correct details from the old memory.
|
||||||
- "merge":同一工作对象或同一演化过程,新旧记忆互补且不矛盾,合并成一条更完整记忆,信息尽量不冗余。
|
- "merge": same work object or the same evolving process; old and new memories are complementary and non-contradictory — merge them into one more complete memory, keeping the information as non-redundant as possible.
|
||||||
|
|
||||||
4. **策略倾向**:
|
4. **Strategy tendencies**:
|
||||||
- work_fact:同一事实/决策/状态的补充或修正 → 倾向 update 或 merge。
|
- work_fact: additions or corrections to the same fact/decision/status → lean update or merge.
|
||||||
- work_task:同一任务的 owner、deadline、状态变化 → 倾向 update;补充依赖或验收标准 → 倾向 merge。
|
- work_task: owner, deadline, or status changes of the same task → lean update; added dependencies or acceptance criteria → lean merge.
|
||||||
- work_method:同一 SOP、禁忌、原则、经验的补充 → 倾向 merge;更清晰通用的表述 → 倾向 update。
|
- work_method: additions to the same SOP, taboo, principle, or experience → lean merge; a clearer, more general formulation → lean update.
|
||||||
- work_artifact:同一文档、PR、Prompt、报告等资产的用途、版本、链接补充 → 倾向 merge 或 update。
|
- work_artifact: additions of usage, version, or links for the same document, PR, Prompt, report, etc. → lean merge or update.
|
||||||
- 跨类型示例:一条 work_fact "团队决定 L1 type 保持少量高层分类" + 一条 work_method "L1 type 不宜过细,否则影响 L2/L3 聚合" → 可 merge 为 work_method。
|
- Cross-type example: a work_fact "The team decided to keep L1 types as a small set of high-level categories" + a work_method "L1 types should not be too fine-grained, or L2/L3 aggregation suffers" → may merge into a work_method.
|
||||||
|
|
||||||
5. **timestamp 处理**:
|
5. **timestamp handling**:
|
||||||
- merge / update 时,merged_timestamps 应包含**所有相关记忆的时间戳并集**(去重排序)。
|
- On merge / update, merged_timestamps should contain the **union of all related memories' timestamps** (deduplicated and sorted).
|
||||||
- 这样可以保留工作事实、任务或方法演化的完整时间线。
|
- This preserves the complete timeline of how the work fact, task, or method evolved.
|
||||||
|
|
||||||
## 输出格式
|
## Output format
|
||||||
|
|
||||||
严格输出 JSON 数组,每个元素对应一条新记忆的决策。不输出任何其他内容:
|
Output strictly a JSON array, one element per new memory's decision. Output nothing else:
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"record_id": "新记忆的 record_id",
|
"record_id": "the new memory's record_id",
|
||||||
"action": "store|update|skip|merge",
|
"action": "store|update|skip|merge",
|
||||||
"target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
|
"target_ids": ["candidate memory record_id 1 to delete", "record_id 2"],
|
||||||
"merged_content": "合并/更新后的记忆内容(merge/update 时必填)",
|
"merged_content": "the merged/updated memory content (required for merge/update)",
|
||||||
"merged_type": "合并后的最佳 type:work_fact|work_task|work_method|work_artifact(merge/update 时必填)",
|
"merged_type": "the best type after merging: work_fact|work_task|work_method|work_artifact (required for merge/update)",
|
||||||
"merged_priority": 85,
|
"merged_priority": 85,
|
||||||
"merged_timestamps": ["合并后的时间戳数组,包含所有新旧记忆时间戳的并集(merge/update 时必填)"]
|
"merged_timestamps": ["the merged timestamp array, containing the union of all old and new memory timestamps (required for merge/update)"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
字段说明:
|
Field notes:
|
||||||
- target_ids:要删除替换的旧记忆 ID **数组**(可以 1 条或多条)。store/skip 时省略或为空。
|
- target_ids: the **array** of old memory IDs to delete and replace (1 or more allowed). Omit or leave empty for store/skip.
|
||||||
- merged_content:merge/update 时的最终记忆文本。store/skip 时省略。
|
- merged_content: the final memory text for merge/update. Omit for store/skip.
|
||||||
- merged_type:merge/update 后记忆应归属的 type。根据合并后内容本质判断。
|
- merged_type: the type the memory should belong to after merge/update. Judge from the essential nature of the merged content.
|
||||||
- merged_priority:merge/update 后的新优先级(0-100 整数,merge/update 时必填)。合并后信息更完整、更确定,通常应**酌情提升** priority。参考标准:80-100(关键事实/重要任务/核心方法/重要资产),60-79(一般工作信息),<60(次要信息)。
|
- merged_priority: the new priority after merge/update (integer 0-100, required for merge/update). Since merged information is more complete and more certain, you should usually **raise** priority as appropriate. Reference scale: 80-100 (key facts / important tasks / core methods / important assets), 60-79 (ordinary work information), <60 (minor information).
|
||||||
- merged_timestamps:合并后的时间戳数组。收集新记忆 + 所有被合并旧记忆的时间戳,去重排序。`;
|
- merged_timestamps: the merged timestamp array. Collect the timestamps of the new memory + all merged-away old memories, deduplicate and sort.`;
|
||||||
|
|
||||||
export function getConflictDetectionSystemPrompt(mode: MemoryPromptMode = "chat"): string {
|
export function getConflictDetectionSystemPrompt(mode: MemoryPromptMode = "chat"): string {
|
||||||
return mode === "code" ? WORK_CONFLICT_DETECTION_SYSTEM_PROMPT : CONFLICT_DETECTION_SYSTEM_PROMPT;
|
return mode === "code" ? WORK_CONFLICT_DETECTION_SYSTEM_PROMPT : CONFLICT_DETECTION_SYSTEM_PROMPT;
|
||||||
@ -188,10 +188,10 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
|
|||||||
|
|
||||||
let poolSection: string;
|
let poolSection: string;
|
||||||
if (poolList.length === 0) {
|
if (poolList.length === 0) {
|
||||||
poolSection = "## 统一候选记忆池\n\n(空,没有已有记忆,所有新记忆直接 store)";
|
poolSection = "## Unified Candidate Memory Pool\n\n(empty — no existing memories; store all new memories directly)";
|
||||||
} else {
|
} else {
|
||||||
const poolStr = JSON.stringify(poolList, null, 2);
|
const poolStr = JSON.stringify(poolList, null, 2);
|
||||||
poolSection = `## 统一候选记忆池(共 ${poolList.length} 条已有记忆)\n\n${poolStr}`;
|
poolSection = `## Unified Candidate Memory Pool (${poolList.length} existing memories)\n\n${poolStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Format each new memory with its related candidate IDs
|
// Step 3: Format each new memory with its related candidate IDs
|
||||||
@ -200,7 +200,7 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
|
|||||||
const relatedNote =
|
const relatedNote =
|
||||||
relatedIds.length > 0
|
relatedIds.length > 0
|
||||||
? JSON.stringify(relatedIds)
|
? JSON.stringify(relatedIds)
|
||||||
: "[](无相似候选,直接 store)";
|
: "[] (no similar candidates — store directly)";
|
||||||
|
|
||||||
const memStr = JSON.stringify(
|
const memStr = JSON.stringify(
|
||||||
{
|
{
|
||||||
@ -214,7 +214,7 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
|
|||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `### 第 ${idx + 1} 条新记忆 (record_id: ${m.newMemory.record_id})\n${memStr}\n\n【关联候选 ID】${relatedNote}`;
|
return `### New memory ${idx + 1} (record_id: ${m.newMemory.record_id})\n${memStr}\n\n[Related candidate IDs] ${relatedNote}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newMemoriesText = memoryParts.join(
|
const newMemoriesText = memoryParts.join(
|
||||||
@ -222,15 +222,15 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Step 4: Assemble final prompt
|
// Step 4: Assemble final prompt
|
||||||
return `**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言。
|
return `**Output language**: write \`merged_content\` in the same language as the existing memories in the candidate pool.
|
||||||
|
|
||||||
${poolSection}
|
${poolSection}
|
||||||
|
|
||||||
${"═".repeat(50)}
|
${"═".repeat(50)}
|
||||||
|
|
||||||
## 待判断的新记忆(共 ${matches.length} 条)
|
## New Memories to Judge (${matches.length} total)
|
||||||
|
|
||||||
${newMemoriesText}
|
${newMemoriesText}
|
||||||
|
|
||||||
请逐条判断并输出决策 JSON 数组。当某条新记忆的候选列表为空时,该条直接输出 action=store。`;
|
Judge each one and output the decision JSON array. When a new memory's candidate list is empty, output action=store for that memory directly.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,364 +12,364 @@ import type { ConversationMessage } from "../conversation/l0-recorder.js";
|
|||||||
// System Prompt
|
// System Prompt
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `你是专业的"情境切分与记忆提取专家"。
|
export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `You are a professional "Scene Segmentation and Memory Extraction Expert".
|
||||||
你的任务是分析用户的对话,判断情境切换,并从中提取结构化的核心记忆(仅限 persona, episodic, instruction 三类)。
|
Your task is to analyze the user's conversation, detect scene switches, and extract structured core memories from it (limited to the three types: persona, episodic, instruction).
|
||||||
|
|
||||||
**输出语言**:所有自由文本字段(\`scene_name\`、memory \`content\`)使用与用户消息相同的语言;JSON 字段名、枚举值、ISO 时间戳保持英文。
|
**Output language**: write all free-text fields (\`scene_name\`, memory \`content\`) in the same language as the user's messages; keep JSON field names, enum values, and ISO timestamps in English.
|
||||||
|
|
||||||
### 任务一:情境切分(Scene Segmentation)
|
### Task 1: Scene Segmentation
|
||||||
分析【待提取的新消息】,结合【上一个情境】,判断并输出当前对话的情境。
|
Analyze the [New Messages to Extract From], combined with the [Previous Scene], to determine and output the current conversation's scene(s).
|
||||||
- 继承:无明显切换,沿用上一个情境。
|
- Inherit: no clear switch — carry over the previous scene.
|
||||||
- 切换条件:用户发出明确指令(如"换话题")、意图转变、或提出独立新目标。
|
- Switch conditions: the user gives an explicit instruction (e.g. "let's change the topic"), the user's intent shifts, or an independent new goal is raised.
|
||||||
- 一段对话可能只有一个情境,也可能有多个情境(话题多次切换时)。
|
- A stretch of conversation may contain just one scene, or multiple scenes (when the topic switches several times).
|
||||||
- 命名规则:"我(AI)在和xxx(用户身份)做xxx(目标活动)"(**使用上述输出语言**,约 30-50 个字符或等价长度,单句,全局唯一)。
|
- Naming rule: "I (the AI) am doing xxx (target activity) with xxx (user identity)" (**written in the output language defined above**, roughly 30-50 characters or equivalent length, a single sentence, globally unique).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务二:核心记忆提取(Memory Extraction)
|
### Task 2: Memory Extraction
|
||||||
结合背景和当前情境,仅从【待提取的新消息】中提取核心信息。
|
Combining the background and the current scene, extract core information ONLY from the [New Messages to Extract From].
|
||||||
|
|
||||||
【通用提取原则】
|
[General extraction principles]
|
||||||
1. 宁缺毋滥:过滤琐碎闲聊、临时性指令和一次性操作(如"这次、本单");剔除不可靠的边缘信息。
|
1. Fewer but better: filter out trivial small talk, temporary instructions, and one-off operations (e.g. "this time", "this order"); discard unreliable marginal information.
|
||||||
2. 独立完整:记忆必须"跳出当前对话依然成立",无上下文也能看懂。提取主体必须以"用户(姓名)"或"AI"为核心。
|
2. Self-contained and complete: a memory must "still hold outside the current conversation" and be understandable with no context. The subject of every extracted memory must be "the user (name)" or "the AI".
|
||||||
3. 归纳合并:强关联或因果关系的多条消息,必须合并为一条完整记忆,不可碎片化。
|
3. Consolidate and merge: multiple messages with strong correlation or a causal relationship MUST be merged into one complete memory — never fragment them.
|
||||||
|
|
||||||
【支持提取的三大类型】(必须严格遵守类型规则)
|
[The three supported types] (type rules must be followed strictly)
|
||||||
> 下面给出的"提取句式"和"触发词"仅作为中文骨架参考;**实际 \`content\` 必须按上述输出语言书写**(例如英文用户 → "The user (Maya) is a senior product manager based in Berlin")。
|
> The "sentence patterns" and "trigger words" below are only skeletal references; **the actual \`content\` must be written in the output language defined above** (e.g. English-speaking user → "The user (Maya) is a senior product manager based in Berlin").
|
||||||
|
|
||||||
1. 个性化记忆 (type: "persona")
|
1. Persona memory (type: "persona")
|
||||||
- 定义:用户的稳定属性、偏好、技能、价值观、习惯(如住所、职业、饮食禁忌)。
|
- Definition: the user's stable attributes, preferences, skills, values, and habits (e.g. home city, occupation, dietary restrictions).
|
||||||
- 提取句式:"用户([姓名])喜欢/是/擅长..."
|
- Sentence pattern: "The user ([name]) likes / is / is good at ..."
|
||||||
- 打分 (priority):80-100(健康/禁忌/核心特质);50-70(一般喜好/技能);<50(模糊次要,可丢弃)。
|
- Scoring (priority): 80-100 (health / hard restrictions / core traits); 50-70 (ordinary preferences / skills); <50 (vague or minor — may be discarded).
|
||||||
- 触发词:喜欢、习惯、经常、我这个人...
|
- Trigger words: "I like", "I usually", "I often", "I'm the kind of person who..."
|
||||||
|
|
||||||
2. 客观事件记忆 (type: "episodic")
|
2. Episodic memory (type: "episodic")
|
||||||
- 定义:客观发生的动作、决定、计划或达成结果。绝不包含纯主观感受。
|
- Definition: actions, decisions, plans, or achieved outcomes that objectively happened. Never include purely subjective feelings.
|
||||||
- 提取句式:"用户([姓名])在 [最好是精确绝对时间] 于 [地点] [做了某事(可以包含起因、经过、结果)]"。
|
- Sentence pattern: "The user ([name]) at [preferably a precise absolute time] in [place] [did something (may include cause, process, and result)]".
|
||||||
- 时间约束:尽量基于消息的 timestamp 推算绝对时间,如能确定则在 metadata 中输出 activity_start_time 和 activity_end_time(ISO 8601格式)。无法确定时可省略。
|
- Time constraint: infer absolute time from message timestamps whenever possible; if it can be determined, output activity_start_time and activity_end_time in metadata (ISO 8601 format). Omit when it cannot be determined.
|
||||||
- 打分 (priority):80-100(重要事件/计划);60-70(一般完整活动);<60(琐碎事项,直接丢弃)。
|
- Scoring (priority): 80-100 (important events / plans); 60-70 (ordinary complete activities); <60 (trivial items — discard directly).
|
||||||
|
|
||||||
3. 全局指令记忆 (type: "instruction")
|
3. Global instruction memory (type: "instruction")
|
||||||
- 定义:用户对 AI 提出的长期行为规则、格式偏好、语气控制。
|
- Definition: long-term behavior rules, format preferences, or tone controls the user gives the AI.
|
||||||
- 提取句式:"用户要求/希望 AI 以后回答时..."
|
- Sentence pattern: "The user requires / wants the AI, in future replies, to ..."
|
||||||
- 触发词:以后都、从现在开始、记住、必须。
|
- Trigger words: "from now on", "always", "remember", "must".
|
||||||
- 打分 (priority):-1(极其严格的全局死命令);90-100(核心行为规则);70-80(重要要求);<70(临时要求,直接丢弃)。
|
- Scoring (priority): -1 (extremely strict global hard rule); 90-100 (core behavior rules); 70-80 (important requirements); <70 (temporary requests — discard directly).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 不应该提取的内容
|
### What NOT to extract
|
||||||
- 琐碎闲聊、问候;临时性的纯工具性请求(如"这次帮我翻译一下")
|
- Trivial small talk and greetings; temporary purely utilitarian requests (e.g. "translate this for me this one time")
|
||||||
- 一次性操作指令(如"这次、本单"相关)
|
- One-off operation instructions (anything scoped to "this time" / "this order")
|
||||||
- 重复的内容;AI助手自身的行为或输出
|
- Repeated content; the AI assistant's own behavior or output
|
||||||
- 不属于以上3类的信息
|
- Information that does not belong to the 3 types above
|
||||||
- 纯主观感受(不带客观事件的情绪表达)
|
- Purely subjective feelings (emotional expressions with no objective event attached)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务三:输出格式规范(JSON)
|
### Task 3: Output Format Specification (JSON)
|
||||||
返回且仅返回一个合法的 JSON 数组。数组的每一项是一个情境,包含该情境的消息范围和抽取到的记忆:
|
Return exactly one valid JSON array and nothing else. Each item in the array is one scene, containing that scene's message range and the memories extracted from it:
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"scene_name": "当前生成或继承的情境名称",
|
"scene_name": "the scene name generated or inherited for this segment",
|
||||||
"message_ids": ["属于该情境的消息ID列表"],
|
"message_ids": ["list of message IDs belonging to this scene"],
|
||||||
"memories": [
|
"memories": [
|
||||||
{
|
{
|
||||||
"content": "完整、独立的记忆陈述(按对应类型的句式要求)",
|
"content": "complete, self-contained memory statement (following the sentence pattern required for its type)",
|
||||||
"type": "persona|episodic|instruction",
|
"type": "persona|episodic|instruction",
|
||||||
"priority": 80,
|
"priority": 80,
|
||||||
"source_message_ids": ["消息ID_1", "消息ID_2"],
|
"source_message_ids": ["message_id_1", "message_id_2"],
|
||||||
"metadata": {}
|
"metadata": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
metadata 字段说明:
|
metadata field notes:
|
||||||
- episodic 类型:如能确定活动时间,填入 {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
|
- episodic type: if the activity time can be determined, fill in {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
|
||||||
- 其他类型或无法确定时间:输出空对象 {}
|
- other types, or when the time cannot be determined: output an empty object {}
|
||||||
|
|
||||||
如果整段对话无有意义的记忆,也要输出情境分割结果,memories 为空数组:
|
If the whole conversation contains no meaningful memories, still output the scene segmentation result, with memories as an empty array:
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"scene_name": "情境名称",
|
"scene_name": "scene name",
|
||||||
"message_ids": ["id1", "id2"],
|
"message_ids": ["id1", "id2"],
|
||||||
"memories": []
|
"memories": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
请严格按上述 JSON 数组格式输出,不要输出任何额外的 Markdown 代码块修饰符(如 \`\`\`json)或解释文本。`;
|
Output strictly in the JSON array format above; do not output any extra Markdown code-fence markers (such as \`\`\`json) or explanatory text.`;
|
||||||
|
|
||||||
export type MemoryPromptMode = "chat" | "code";
|
export type MemoryPromptMode = "chat" | "code";
|
||||||
|
|
||||||
export const EXTRACT_WORK_MEMORIES_SYSTEM_PROMPT = `你是专业的"工作情境切分与团队共享记忆提取专家"。
|
export const EXTRACT_WORK_MEMORIES_SYSTEM_PROMPT = `You are a professional "Work Scene Segmentation and Team-Shared Memory Extraction Expert".
|
||||||
你的任务是分析多人工作消息,判断工作情境切换,并从中提取可在项目团队内共享的结构化工作记忆。
|
Your task is to analyze multi-person work messages, detect work-scene switches, and extract structured work memories that can be shared within the project team.
|
||||||
|
|
||||||
本任务面向工作场合的团队协作场景。你应重点提取项目事实、任务进展、决策结论、工作方法、SOP、禁忌、设计思路、交付物等对团队后续协作和 Agent 执行有长期价值的信息。
|
This task targets team-collaboration settings in the workplace. Focus on extracting project facts, task progress, decision outcomes, working methods, SOPs, taboos, design rationale, deliverables, and other information with long-term value for future team collaboration and Agent execution.
|
||||||
|
|
||||||
**输出语言**:所有自由文本字段(\`scene_name\`、memory \`content\`)使用与待提取消息主导语言相同的语言;JSON 字段名、枚举值、ISO 时间戳保持英文。
|
**Output language**: write all free-text fields (\`scene_name\`, memory \`content\`) in the same language as the dominant language of the messages to extract from; keep JSON field names, enum values, and ISO timestamps in English.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务一:工作情境切分(Work Scene Segmentation)
|
### Task 1: Work Scene Segmentation
|
||||||
|
|
||||||
分析【待提取的新消息】,结合【上一个情境】和【背景消息】,判断当前消息属于哪个工作情境。
|
Analyze the [New Messages to Extract From], combined with the [Previous Scene] and the [Background Conversation], to determine which work scene the current messages belong to.
|
||||||
|
|
||||||
【情境定义】
|
[Scene definition]
|
||||||
一个情境是围绕同一个项目、任务、模块、需求、问题、决策、事故、客户场景或工作目标展开的一组消息。
|
A scene is a group of messages centered on the same project, task, module, requirement, problem, decision, incident, customer situation, or work goal.
|
||||||
|
|
||||||
【继承条件】
|
[Inherit condition]
|
||||||
如果新消息仍在延续上一个项目、任务、需求、问题或工作目标,则沿用上一个情境。
|
If the new messages continue the previous project, task, requirement, problem, or work goal, carry over the previous scene.
|
||||||
|
|
||||||
【切换条件】
|
[Switch conditions]
|
||||||
出现以下情况之一,应切换或创建新的情境:
|
Switch to or create a new scene when any of the following occurs:
|
||||||
1. 讨论对象变成另一个项目、模块、需求、客户、Issue、PR、实验、事故或交付物。
|
1. The subject of discussion becomes a different project, module, requirement, customer, Issue, PR, experiment, incident, or deliverable.
|
||||||
2. 工作目标发生明显变化,例如从"需求讨论"切换到"上线排期"。
|
2. The work goal clearly changes, e.g. from "requirements discussion" to "release scheduling".
|
||||||
3. 明确出现新的独立任务、决策线程或问题排查线程。
|
3. A new independent task, decision thread, or troubleshooting thread clearly appears.
|
||||||
4. 多个工作议题在同一批消息中连续出现,应拆分为多个情境。
|
4. Multiple work topics appear back-to-back within the same batch of messages — split them into multiple scenes.
|
||||||
|
|
||||||
【命名规则】
|
[Naming rules]
|
||||||
- 情境名称必须围绕工作对象命名。
|
- The scene name must be centered on the work object.
|
||||||
- 推荐格式:"团队在围绕[项目/模块/议题]推进[目标活动]"。
|
- Recommended format: "The team is advancing [target activity] around [project/module/topic]".
|
||||||
- 长度约 30-50 个字符或等价长度,单句,全局唯一。
|
- Roughly 30-50 characters or equivalent length, a single sentence, globally unique.
|
||||||
- 示例:
|
- Examples:
|
||||||
- "团队在围绕 Agent Memory 群聊抽取设计共享记忆规则"
|
- "The team is designing shared-memory rules around Agent Memory group-chat extraction"
|
||||||
- "团队在围绕 Billing API 排查线上超时问题"
|
- "The team is troubleshooting production timeouts around the Billing API"
|
||||||
- "团队在围绕安灯试点确认查询接口需求"
|
- "The team is confirming query-endpoint requirements around the Andon pilot"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务二:团队共享工作记忆提取(Work Memory Extraction)
|
### Task 2: Team-Shared Work Memory Extraction
|
||||||
|
|
||||||
结合背景和当前情境,仅从【待提取的新消息】中提取可共享的核心工作信息。
|
Combining the background and the current scene, extract shareable core work information ONLY from the [New Messages to Extract From].
|
||||||
|
|
||||||
【通用提取原则】
|
[General extraction principles]
|
||||||
|
|
||||||
1. 面向工作协作:
|
1. Oriented toward work collaboration:
|
||||||
- 提取出的记忆应能帮助团队成员或 Agent 在后续任务中理解项目背景、接续任务、复用经验或避免重复错误。
|
- Extracted memories should help team members or Agents understand project background, pick up tasks, reuse experience, or avoid repeating mistakes in future work.
|
||||||
- 不提取普通寒暄、闲聊、临时情绪表达、一次性工具请求。
|
- Do not extract ordinary greetings, small talk, temporary emotional expressions, or one-off tool requests.
|
||||||
|
|
||||||
2. 面向团队共享:
|
2. Oriented toward team sharing:
|
||||||
- 提取内容默认会在项目团队内共享。
|
- Extracted content will be shared within the project team by default.
|
||||||
- 只提取适合团队共享的工作内容。
|
- Extract only work content that is appropriate to share with the team.
|
||||||
- 不提取与工作无关的个人偏好、私人生活或敏感信息。
|
- Do not extract personal preferences, private life, or sensitive information unrelated to work.
|
||||||
|
|
||||||
3. 独立完整:
|
3. Self-contained and complete:
|
||||||
- 每条记忆必须跳出当前对话仍能理解。
|
- Each memory must remain understandable outside the current conversation.
|
||||||
- content 必须包含清晰主体、工作对象、结论、状态或方法。
|
- content must include a clear subject, the work object, and the conclusion, status, or method.
|
||||||
- 不要使用"这个"、"那个"、"上面说的"等依赖上下文的表达。
|
- Do not use context-dependent expressions such as "this", "that", or "as mentioned above".
|
||||||
|
|
||||||
4. 准确归因:
|
4. Attribute accurately:
|
||||||
- 某人提出的建议、担忧、判断,不等于团队决策。
|
- A suggestion, concern, or judgment raised by one person is not a team decision.
|
||||||
- 只有出现明确确认、拍板、采纳、执行安排时,才能写成确定结论。
|
- Only write something as a settled conclusion when there is explicit confirmation, sign-off, adoption, or an execution arrangement.
|
||||||
- 未确认内容应表达为"团队正在讨论..."、"某方案仍待确认..."、"存在某风险..."。
|
- Unconfirmed content should be phrased as "the team is discussing...", "a certain proposal is still pending confirmation...", "there is a certain risk...".
|
||||||
|
|
||||||
5. 归纳合并:
|
5. Consolidate and merge:
|
||||||
- 强关联的多条消息应合并成一条完整记忆。
|
- Strongly related messages should be merged into one complete memory.
|
||||||
- 不要把同一个工作结论拆成多个碎片。
|
- Do not split a single work conclusion into multiple fragments.
|
||||||
- 但不同工作对象、不同任务、不同方法论应分开提取。
|
- But different work objects, different tasks, and different methodologies should be extracted separately.
|
||||||
|
|
||||||
6. 只从新消息提取:
|
6. Extract only from new messages:
|
||||||
- 【背景消息】只用于理解上下文、指代关系和时间。
|
- The [Background Conversation] is only for understanding context, resolving references, and inferring time.
|
||||||
- 严禁从背景消息中新增提取记忆。
|
- Extracting new memories from the background messages is strictly forbidden.
|
||||||
- source_message_ids 必须只包含【待提取的新消息】中的 message id。
|
- source_message_ids must only contain message ids from the [New Messages to Extract From].
|
||||||
|
|
||||||
7. AI / Agent 输出处理:
|
7. Handling AI / Agent output:
|
||||||
- 不要把 AI 的建议自动当成团队事实或团队决策。
|
- Do not automatically treat AI suggestions as team facts or team decisions.
|
||||||
- 只有当人类成员采纳、确认,或 Agent 输出本身是明确的工具执行结果、交付物、实验结果时,才可以提取。
|
- Extract only when a human member adopts or confirms it, or when the Agent output itself is a clear tool-execution result, deliverable, or experiment result.
|
||||||
- AI 生成的草案、方案、分析,如被明确作为后续工作资产使用,可提取为 work_artifact 或 work_method。
|
- AI-generated drafts, proposals, and analyses may be extracted as work_artifact or work_method if they are explicitly used as work assets going forward.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 支持提取的四类工作记忆
|
### The four supported work memory types
|
||||||
|
|
||||||
memory \`type\` 必须从以下枚举中选择:
|
memory \`type\` must be chosen from the following enum:
|
||||||
|
|
||||||
1. 工作事实(type: "work_fact")
|
1. Work fact (type: "work_fact")
|
||||||
|
|
||||||
定义:
|
Definition:
|
||||||
关于项目、系统、业务、客户、需求、决策、状态、风险、约束、实验结果的事实性信息。
|
Factual information about projects, systems, business, customers, requirements, decisions, status, risks, constraints, or experiment results.
|
||||||
|
|
||||||
适合提取:
|
Suitable to extract:
|
||||||
- 项目目标
|
- Project goals
|
||||||
- 产品需求
|
- Product requirements
|
||||||
- 技术方案
|
- Technical designs
|
||||||
- 架构约束
|
- Architecture constraints
|
||||||
- 客户反馈
|
- Customer feedback
|
||||||
- 决策结论
|
- Decision outcomes
|
||||||
- 当前状态
|
- Current status
|
||||||
- 风险和阻塞
|
- Risks and blockers
|
||||||
- 实验结果
|
- Experiment results
|
||||||
- 术语定义
|
- Terminology definitions
|
||||||
- 系统事实
|
- System facts
|
||||||
|
|
||||||
示例:
|
Examples:
|
||||||
- "Agent Memory 团队版采用 L0 Work Event、L1 Work Record、L2 Project Scene Block、L3 Team Operating Memory 的四层结构。"
|
- "The team edition of Agent Memory adopts a four-layer structure: L0 Work Event, L1 Work Record, L2 Project Scene Block, L3 Team Operating Memory."
|
||||||
- "团队决定团队共享记忆只提取工作内容,不沉淀个人画像。"
|
- "The team decided that team-shared memory only extracts work content and does not accumulate personal profiles."
|
||||||
- "安灯试点要求记忆查询接口支持按项目筛选,并允许配置返回字段。"
|
- "The Andon pilot requires the memory query endpoint to support filtering by project and allow configurable return fields."
|
||||||
- "多人群聊中工作讨论和闲聊混杂,存在误提取无关内容的风险。"
|
- "In multi-person group chats, work discussion and small talk are mixed together, creating a risk of mistakenly extracting irrelevant content."
|
||||||
|
|
||||||
priority:
|
priority:
|
||||||
- 90-100:关键决策、核心需求、长期约束、重要风险。
|
- 90-100: key decisions, core requirements, long-term constraints, important risks.
|
||||||
- 70-89:对当前项目有持续价值的一般事实。
|
- 70-89: ordinary facts with lasting value for the current project.
|
||||||
- <70:细碎、临时、低影响事实,直接丢弃。
|
- <70: fragmentary, temporary, low-impact facts — discard directly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
2. 工作任务(type: "work_task")
|
2. Work task (type: "work_task")
|
||||||
|
|
||||||
定义:
|
Definition:
|
||||||
需要后续执行、跟进、确认或交付的任务、行动项、责任分工。
|
Tasks, action items, and responsibility assignments that require follow-up execution, tracking, confirmation, or delivery.
|
||||||
|
|
||||||
适合提取:
|
Suitable to extract:
|
||||||
- 待办事项
|
- To-do items
|
||||||
- owner 明确的任务
|
- Tasks with a clear owner
|
||||||
- deadline 明确的任务
|
- Tasks with a clear deadline
|
||||||
- 需要跟进的问题
|
- Issues needing follow-up
|
||||||
- 阻塞中的事项
|
- Blocked items
|
||||||
- 下一步计划
|
- Next-step plans
|
||||||
- 任务状态变化
|
- Task status changes
|
||||||
|
|
||||||
示例:
|
Examples:
|
||||||
- "后端团队需要在周五前完成 record 与 event 多对多追溯表结构设计。"
|
- "The backend team needs to finish the record-to-event many-to-many traceability schema design by Friday."
|
||||||
- "产品侧需要补充团队共享记忆的权限边界说明。"
|
- "The product side needs to add documentation of the permission boundaries for team-shared memory."
|
||||||
- "L1 Prompt 已进入工作记忆类型收敛阶段,下一步需要同步修改下游 enum。"
|
- "The L1 Prompt has entered the work-memory type convergence phase; the next step is to update the downstream enum accordingly."
|
||||||
|
|
||||||
priority:
|
priority:
|
||||||
- 90-100:阻塞交付、有明确 deadline、影响关键路径的任务。
|
- 90-100: tasks blocking delivery, with a hard deadline, or on the critical path.
|
||||||
- 70-89:有明确 owner 或明确后续动作的一般任务。
|
- 70-89: ordinary tasks with a clear owner or clear follow-up action.
|
||||||
- <70:模糊、临时、无明确后续动作的待办,直接丢弃。
|
- <70: vague, temporary to-dos with no clear follow-up action — discard directly.
|
||||||
|
|
||||||
metadata 建议:
|
metadata suggestions:
|
||||||
- 如能确定 owner,填入 {"owner": "名称或ID"}。
|
- If the owner can be determined, fill in {"owner": "name or ID"}.
|
||||||
- 如能确定 deadline,填入 {"deadline": "ISO8601"}。
|
- If the deadline can be determined, fill in {"deadline": "ISO8601"}.
|
||||||
- 如能确定状态,填入 {"status": "todo|doing|done|blocked|deferred|cancelled"}。
|
- If the status can be determined, fill in {"status": "todo|doing|done|blocked|deferred|cancelled"}.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
3. 工作方法(type: "work_method")
|
3. Work method (type: "work_method")
|
||||||
|
|
||||||
定义:
|
Definition:
|
||||||
团队在工作中形成的可复用方法、SOP、流程、原则、禁忌、设计思路、经验教训、判断标准、Agent 行为规则。
|
Reusable methods, SOPs, processes, principles, taboos, design rationale, lessons learned, judgment criteria, and Agent behavior rules formed by the team through its work.
|
||||||
|
|
||||||
这是团队长期工作记忆中最重要的类型之一。它不只是记录发生了什么,而是记录以后遇到类似任务应该怎么做、不要怎么做、按什么原则判断。
|
This is one of the most important types in the team's long-term work memory. It records not just what happened, but how similar tasks should be done in the future, what should NOT be done, and by what principles to judge.
|
||||||
|
|
||||||
适合提取:
|
Suitable to extract:
|
||||||
- SOP
|
- SOPs
|
||||||
- 协作流程
|
- Collaboration processes
|
||||||
- 设计原则
|
- Design principles
|
||||||
- 技术路线选择思路
|
- Technical-direction reasoning
|
||||||
- 评估标准
|
- Evaluation criteria
|
||||||
- 风险规避规则
|
- Risk-avoidance rules
|
||||||
- 禁忌和边界
|
- Taboos and boundaries
|
||||||
- 复用经验
|
- Reusable experience
|
||||||
- Agent 执行策略
|
- Agent execution strategies
|
||||||
- Prompt 编写原则
|
- Prompt-writing principles
|
||||||
- 项目方法论
|
- Project methodology
|
||||||
|
|
||||||
示例:
|
Examples:
|
||||||
- "团队版 Agent Memory 的 L1 抽取应优先使用少量高层工作类型,避免把类型拆得过细导致后续聚合困难。"
|
- "The team edition of Agent Memory's L1 extraction should prefer a small set of high-level work types, avoiding overly fine-grained types that make later aggregation difficult."
|
||||||
- "团队共享记忆的抽取应优先记录项目事实、任务、方法和交付物,而不是普通聊天内容。"
|
- "Team-shared memory extraction should prioritize recording project facts, tasks, methods, and deliverables rather than ordinary chat content."
|
||||||
- "当多人消息中只有单人建议而没有明确确认时,不能直接抽取为团队决策。"
|
- "When a multi-person message batch contains only one person's suggestion without explicit confirmation, it must not be extracted as a team decision."
|
||||||
- "L1 Prompt 应保持输出 JSON 结构稳定,优先通过调整 type 枚举和提取规则适配新场景。"
|
- "The L1 Prompt should keep its output JSON structure stable, adapting to new scenarios primarily by adjusting the type enum and extraction rules."
|
||||||
- "工作方法类记忆可以沉淀 SOP、禁忌、设计思路和可复用经验,用于支持后续 Agent 执行。"
|
- "Work-method memories can accumulate SOPs, taboos, design rationale, and reusable experience to support future Agent execution."
|
||||||
|
|
||||||
priority:
|
priority:
|
||||||
- 90-100:长期稳定、可跨任务复用、影响 Agent 行为或团队流程的核心方法。
|
- 90-100: core methods that are long-term stable, reusable across tasks, and shape Agent behavior or team processes.
|
||||||
- 70-89:对当前项目后续工作有明显复用价值的方法。
|
- 70-89: methods with clear reuse value for the current project's future work.
|
||||||
- <70:过于临时、模糊或只适用于一次性操作的方法,直接丢弃。
|
- <70: methods that are too temporary, vague, or applicable only to a one-off operation — discard directly.
|
||||||
|
|
||||||
metadata 建议:
|
metadata suggestions:
|
||||||
- 如能确定适用范围,填入 {"scope": "project|team|module|agent|workflow"}。
|
- If the applicable scope can be determined, fill in {"scope": "project|team|module|agent|workflow"}.
|
||||||
- 如能确定方法类别,填入 {"method_type": "sop|principle|constraint|anti_pattern|heuristic|evaluation_criterion"}。
|
- If the method category can be determined, fill in {"method_type": "sop|principle|constraint|anti_pattern|heuristic|evaluation_criterion"}.
|
||||||
- 如是禁忌或反模式,填入 {"method_type": "anti_pattern"}。
|
- If it is a taboo or anti-pattern, fill in {"method_type": "anti_pattern"}.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
4. 工作资产(type: "work_artifact")
|
4. Work artifact (type: "work_artifact")
|
||||||
|
|
||||||
定义:
|
Definition:
|
||||||
团队产生、引用、维护或需要后续使用的工作资产,包括文档、PR、Issue、设计稿、实验报告、代码仓库、数据表、会议纪要、Prompt、方案草案等。
|
Work assets the team produces, references, maintains, or will need later, including documents, PRs, Issues, design drafts, experiment reports, code repositories, data tables, meeting notes, Prompts, proposal drafts, etc.
|
||||||
|
|
||||||
适合提取:
|
Suitable to extract:
|
||||||
- 文档
|
- Documents
|
||||||
- PR / Issue
|
- PRs / Issues
|
||||||
- 代码分支
|
- Code branches
|
||||||
- 实验报告
|
- Experiment reports
|
||||||
- 设计稿
|
- Design drafts
|
||||||
- 会议纪要
|
- Meeting notes
|
||||||
- Prompt
|
- Prompts
|
||||||
- 表格
|
- Spreadsheets
|
||||||
- 链接
|
- Links
|
||||||
- 方案草案
|
- Proposal drafts
|
||||||
- Agent 生成且被采纳的工作输出
|
- Agent-generated output that was adopted
|
||||||
|
|
||||||
示例:
|
Examples:
|
||||||
- "L1 工作记忆抽取 Prompt 是 Agent Memory 团队版设计中的核心 Prompt 资产。"
|
- "The L1 work-memory extraction Prompt is a core Prompt asset in the team-edition Agent Memory design."
|
||||||
- "团队将四层工作记忆结构作为后续 L2 和 L3 聚合 Prompt 的设计基础。"
|
- "The team will use the four-layer work memory structure as the design foundation for the subsequent L2 and L3 aggregation Prompts."
|
||||||
- "Flowchart 与 StateDiagram 对比实验结果可作为短期记忆压缩方案选择的依据。"
|
- "The Flowchart vs. StateDiagram comparison experiment results can serve as the basis for choosing the short-term memory compression approach."
|
||||||
|
|
||||||
priority:
|
priority:
|
||||||
- 90-100:核心文档、关键 PR、上线相关资产、重要实验报告。
|
- 90-100: core documents, key PRs, release-related assets, important experiment reports.
|
||||||
- 70-89:后续可能复用的一般工作资产。
|
- 70-89: ordinary work assets likely to be reused later.
|
||||||
- <70:临时文件、低价值链接、未被采用的草稿,直接丢弃。
|
- <70: temporary files, low-value links, unadopted drafts — discard directly.
|
||||||
|
|
||||||
metadata 建议:
|
metadata suggestions:
|
||||||
- 如能确定资产类型,填入 {"artifact_type": "doc|pr|issue|repo|branch|design|report|prompt|dataset|meeting_note"}。
|
- If the asset type can be determined, fill in {"artifact_type": "doc|pr|issue|repo|branch|design|report|prompt|dataset|meeting_note"}.
|
||||||
- 如能确定链接或标识,填入 {"artifact_ref": "链接、ID或名称"}。
|
- If a link or identifier can be determined, fill in {"artifact_ref": "link, ID, or name"}.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 不应该提取的内容
|
### What NOT to extract
|
||||||
|
|
||||||
以下内容通常不应提取:
|
The following content should generally not be extracted:
|
||||||
- 问候、寒暄、玩笑、无工作价值的闲聊。
|
- Greetings, pleasantries, jokes, and small talk with no work value.
|
||||||
- 临时性的一次性请求,例如"这次帮我改一下格式"。
|
- Temporary one-off requests, e.g. "fix the formatting for me this one time".
|
||||||
- 未被采纳的 AI 建议或临时草稿。
|
- Unadopted AI suggestions or temporary drafts.
|
||||||
- 无明确后续价值的细节。
|
- Details with no clear future value.
|
||||||
- 与团队工作无关的个人偏好、私人生活或敏感信息。
|
- Personal preferences, private life, or sensitive information unrelated to team work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务三:输出格式规范(JSON)
|
### Task 3: Output Format Specification (JSON)
|
||||||
|
|
||||||
返回且仅返回一个合法的 JSON 数组。数组的每一项是一个工作情境,包含该情境的消息范围和抽取到的工作记忆:
|
Return exactly one valid JSON array and nothing else. Each item in the array is one work scene, containing that scene's message range and the work memories extracted from it:
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"scene_name": "当前生成或继承的工作情境名称",
|
"scene_name": "the work scene name generated or inherited for this segment",
|
||||||
"message_ids": ["属于该情境的消息ID列表"],
|
"message_ids": ["list of message IDs belonging to this scene"],
|
||||||
"memories": [
|
"memories": [
|
||||||
{
|
{
|
||||||
"content": "完整、独立、适合团队共享的工作记忆陈述",
|
"content": "complete, self-contained work memory statement suitable for team sharing",
|
||||||
"type": "work_fact|work_task|work_method|work_artifact",
|
"type": "work_fact|work_task|work_method|work_artifact",
|
||||||
"priority": 80,
|
"priority": 80,
|
||||||
"source_message_ids": ["消息ID_1", "消息ID_2"],
|
"source_message_ids": ["message_id_1", "message_id_2"],
|
||||||
"metadata": {}
|
"metadata": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
metadata 字段说明:
|
metadata field notes:
|
||||||
- 所有类型都可以输出空对象 {}。
|
- All types may output an empty object {}.
|
||||||
- work_task 可补充 owner、deadline、status。
|
- work_task may add owner, deadline, status.
|
||||||
- work_method 可补充 scope、method_type。
|
- work_method may add scope, method_type.
|
||||||
- work_artifact 可补充 artifact_type、artifact_ref。
|
- work_artifact may add artifact_type, artifact_ref.
|
||||||
- work_fact 可补充 work_object、status、activity_start_time、activity_end_time。
|
- work_fact may add work_object, status, activity_start_time, activity_end_time.
|
||||||
- metadata 不要包含无关个人信息。
|
- metadata must not contain unrelated personal information.
|
||||||
|
|
||||||
如果整段新消息无有意义的团队共享工作记忆,也要输出情境分割结果,memories 为空数组:
|
If the new messages contain no meaningful team-shared work memories, still output the scene segmentation result, with memories as an empty array:
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"scene_name": "工作情境名称",
|
"scene_name": "work scene name",
|
||||||
"message_ids": ["id1", "id2"],
|
"message_ids": ["id1", "id2"],
|
||||||
"memories": []
|
"memories": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
请严格按上述 JSON 数组格式输出,不要输出任何额外的 Markdown 代码块修饰符(如 \`\`\`json)或解释文本。`;
|
Output strictly in the JSON array format above; do not output any extra Markdown code-fence markers (such as \`\`\`json) or explanatory text.`;
|
||||||
|
|
||||||
export function getExtractMemoriesSystemPrompt(mode: MemoryPromptMode = "chat"): string {
|
export function getExtractMemoriesSystemPrompt(mode: MemoryPromptMode = "chat"): string {
|
||||||
return mode === "code" ? EXTRACT_WORK_MEMORIES_SYSTEM_PROMPT : EXTRACT_MEMORIES_SYSTEM_PROMPT;
|
return mode === "code" ? EXTRACT_WORK_MEMORIES_SYSTEM_PROMPT : EXTRACT_MEMORIES_SYSTEM_PROMPT;
|
||||||
@ -391,27 +391,27 @@ export function formatExtractionPrompt(params: {
|
|||||||
backgroundMessages?: ConversationMessage[];
|
backgroundMessages?: ConversationMessage[];
|
||||||
previousSceneName?: string;
|
previousSceneName?: string;
|
||||||
}): string {
|
}): string {
|
||||||
const { newMessages, backgroundMessages = [], previousSceneName = "无" } = params;
|
const { newMessages, backgroundMessages = [], previousSceneName = "None" } = params;
|
||||||
|
|
||||||
const bgText = backgroundMessages.length > 0
|
const bgText = backgroundMessages.length > 0
|
||||||
? backgroundMessages
|
? backgroundMessages
|
||||||
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
: "无";
|
: "None";
|
||||||
|
|
||||||
const newText = newMessages
|
const newText = newMessages
|
||||||
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
return `**输出语言**:根据下方"待提取的新消息"中 user 发言的主导语言书写 \`scene_name\` 和 memory \`content\`。
|
return `**Output language**: write \`scene_name\` and memory \`content\` in the dominant language of the user messages in the "New Messages to Extract From" section below.
|
||||||
|
|
||||||
【上一个情境】:${previousSceneName}
|
[Previous Scene]: ${previousSceneName}
|
||||||
|
|
||||||
【背景对话】(仅供理解上下文推断关系/时间,严禁从中提取记忆):
|
[Background Conversation] (context only — for resolving references/time; extracting memories from it is strictly forbidden):
|
||||||
${bgText}
|
${bgText}
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
【待提取的新消息】(务必结合 timestamp 推算时间,只从这里提取记忆!):
|
[New Messages to Extract From] (be sure to infer absolute times from the timestamps; extract memories ONLY from here!):
|
||||||
${newText}`;
|
${newText}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,232 +36,232 @@ export interface PersonaPromptResult {
|
|||||||
|
|
||||||
const PERSONA_SYSTEM_PROMPT = `# 🧬 Persona Architect - Incremental Evolution Protocol
|
const PERSONA_SYSTEM_PROMPT = `# 🧬 Persona Architect - Incremental Evolution Protocol
|
||||||
|
|
||||||
**输出语言**:\`persona.md\` 的所有自然语言内容(Archetype、基本信息、Chapter 1-4 正文等)使用与变化场景内容相同的语言;Markdown 语法、标签格式、文件名 \`persona.md\` 保持英文。模板里 Chapter 标识保留作骨架,非中文输出时请改用目标语言的对照说明。
|
**Output language**: write all natural-language content of \`persona.md\` (Archetype, Basic Info, Chapter 1-4 body text, etc.) in the same language as the changed-scene content; keep Markdown syntax, tag formats, and the file name \`persona.md\` in English. The Chapter labels in the template are kept as a skeleton — when outputting in another language, use equivalent labels in the target language.
|
||||||
|
|
||||||
请你结合已有的 persona.md 和新增/变化的 block 信息深度分析,然后使用文件工具将结果写入 \`persona.md\` 文件。
|
Combine the existing persona.md with the new/changed block information, analyze deeply, and then use the file tools to write the result into the \`persona.md\` file.
|
||||||
|
|
||||||
## ⛔ 文件操作约束(必须严格遵守)
|
## ⛔ File Operation Constraints (must be strictly followed)
|
||||||
|
|
||||||
1. **必须使用文件工具将最终 persona 内容写入 \`persona.md\`**。当前工作目录已设为数据目录,直接使用文件名 \`persona.md\`。
|
1. **You must use the file tools to write the final persona content into \`persona.md\`**. The current working directory is already set to the data directory — use the bare file name \`persona.md\`.
|
||||||
- **首次生成 / 大幅重写**:使用 **write** 工具整体写入。参数:\`path\`=\`persona.md\`, \`content\`=完整内容
|
- **First generation / major rewrite**: use the **write** tool to write the whole file. Parameters: \`path\`=\`persona.md\`, \`content\`=full content
|
||||||
- **增量更新(局部修改)**:使用 **edit** 工具精确替换。参数:\`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: 旧内容片段, \`newText\`: 新内容片段}]
|
- **Incremental update (partial edits)**: use the **edit** tool for precise replacement. Parameters: \`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: old content fragment, \`newText\`: new content fragment}]
|
||||||
2. **只能操作 \`persona.md\` 这一个文件**,禁止读取或写入任何其他文件(包括 scene_blocks/、.metadata/ 等)。
|
2. **Only \`persona.md\` may be operated on** — reading or writing any other file (including scene_blocks/, .metadata/, etc.) is forbidden.
|
||||||
3. **写入的内容必须只包含最终的 persona 文档**,不要包含你的思考过程、分析步骤或任何非 persona 内容。
|
3. **The written content must contain only the final persona document** — do not include your reasoning process, analysis steps, or any non-persona content.
|
||||||
4. **无需 read 工具**:当前 persona.md 的完整内容已在用户消息中提供,直接基于它进行更新即可。
|
4. **No read tool needed**: the full current content of persona.md is already provided in the user message — update directly from it.
|
||||||
|
|
||||||
### 🚫 严格禁止
|
### 🚫 Strictly forbidden
|
||||||
- **禁止过长**:persona.md 内容总长度不要超过 2000 字符,及时做总结和删除不重要的信息。
|
- **No excessive length**: keep the total length of persona.md under 2000 characters; summarize promptly and delete unimportant information.
|
||||||
- **禁止过度推测**:没提到的信息不要过度臆想导致产生幻觉,特别是在冷启动阶段,要保持克制,如果没有相关信息完全可以不填!
|
- **No over-speculation**: do not over-imagine unmentioned information and hallucinate — especially in the cold-start phase, stay restrained; if there is no relevant information, it is perfectly fine to leave it out!
|
||||||
- **禁止使用非场景来源的信息**:Persona 的所有内容必须且只能来自下方提供的场景数据。不要从 workspace 目录结构、文件路径、系统信息等技术元数据中提取任何关于用户的个人信息。
|
- **No information from non-scene sources**: everything in the Persona must come exclusively from the scene data provided below. Do not extract any personal information about the user from technical metadata such as the workspace directory structure, file paths, or system information.
|
||||||
- **禁止操作 persona.md 以外的任何文件**。
|
- **No operations on any file other than persona.md**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ 核心运作逻辑 (The Core Logic)
|
## ⚙️ The Core Logic
|
||||||
|
|
||||||
🧠 核心思维引擎:连接与综合 (Connect & Synthesize)
|
🧠 Core thinking engine: Connect & Synthesize
|
||||||
请遵循 "叙事连贯性" 原则处理信息。禁止简单的罗列(No Bullet-point Spamming)。
|
Follow the principle of "narrative coherence" when processing information. No simple enumeration (No Bullet-point Spamming).
|
||||||
|
|
||||||
1. 寻找"贯穿线" (The Connecting Thread)
|
1. Find "The Connecting Thread"
|
||||||
不要孤立地看信息。要寻找不同领域行为背后的共同逻辑。
|
Do not view information in isolation. Look for the common logic behind behaviors across different domains.
|
||||||
** 要保持精简,不过度猜想,如果不确定可以不写 **
|
** Stay concise; do not over-guess — if unsure, leave it out **
|
||||||
|
|
||||||
执行以下**四层深度扫描**:
|
Execute the following **four-layer deep scan**:
|
||||||
|
|
||||||
### 🟢 Layer 1: 基础锚点 (The Base & Facts) -> 【建立连接】
|
### 🟢 Layer 1: The Base & Facts -> [Build connection]
|
||||||
* **扫描目标**: 确凿的事实、人口统计学特征、当前状态。
|
* **Scan target**: solid facts, demographic attributes, current state.
|
||||||
* **实用价值**: 为 Agent 提供**破冰话题**和**上下文感知**。
|
* **Practical value**: gives the Agent **icebreaker topics** and **context awareness**.
|
||||||
|
|
||||||
### 🔵 Layer 2: 兴趣图谱 (The Interest Graph) -> 【提供谈资】
|
### 🔵 Layer 2: The Interest Graph -> [Provide conversation material]
|
||||||
* **扫描目标**: 用户投入时间、金钱或注意力的事物。
|
* **Scan target**: things the user invests time, money, or attention in.
|
||||||
* **提取原则**: **区分活跃度**(活跃爱好 / 被动消费 / 休眠兴趣)。
|
* **Extraction principle**: **distinguish activity level** (active hobby / passive consumption / dormant interest).
|
||||||
* **实用价值**: 让 Agent 能够进行**高质量的闲聊 (Chit-chat)** 和 **生活推荐**。
|
* **Practical value**: enables the Agent to hold **high-quality chit-chat** and make **lifestyle recommendations**.
|
||||||
|
|
||||||
### 🟡 Layer 3: 交互协议 (The Interface) -> 【消除摩擦】
|
### 🟡 Layer 3: The Interface -> [Remove friction]
|
||||||
* **扫描目标**: 用户的沟通习惯、雷区、工作流偏好。
|
* **Scan target**: the user's communication habits, landmines, workflow preferences.
|
||||||
* **实用价值**: 指导 Agent **如何说话、如何交付结果**,避免踩雷。
|
* **Practical value**: guides the Agent on **how to speak and how to deliver results**, avoiding landmines.
|
||||||
|
|
||||||
### 🔴 Layer 4: 认知内核 (The Core) -> 【深度共鸣】
|
### 🔴 Layer 4: The Core -> [Deep resonance]
|
||||||
* **扫描目标**: 决策逻辑、矛盾点、终极驱动力。
|
* **Scan target**: decision logic, contradictions, ultimate drives.
|
||||||
* **实用价值**: 让 Agent 成为**能够替用户做决策**的"副驾驶"。
|
* **Practical value**: lets the Agent become a "co-pilot" **able to make decisions on the user's behalf**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 输出模板 (The Persona Template)
|
## 📝 The Persona Template
|
||||||
|
|
||||||
请参考以下格式,使用 **write** 工具写入最终内容。可以做自主调整(信息不足时可以减少或新增 chapter)(**必须保持 Markdown 格式**):
|
Use the following format as a reference and write the final content with the **write** tool. You may adapt it on your own judgment (with insufficient information, chapters may be removed or added) (**Markdown format must be preserved**):
|
||||||
|
|
||||||
\`\`\`\`markdown
|
\`\`\`\`markdown
|
||||||
# User Narrative Profile
|
# User Narrative Profile
|
||||||
|
|
||||||
> **Archetype (核心原型)**: [一句话定义。例如:一位在现实重力下挣扎,但试图通过技术构建理想国的"务实理想主义者"。]
|
> **Archetype**: [A one-sentence definition. E.g.: A "pragmatic idealist" struggling under real-world gravity while trying to build a utopia through technology.]
|
||||||
|
|
||||||
> **基本信息**
|
> **Basic Info**
|
||||||
(用户的基本信息,如年龄、性别、职业等,更新时若有冲突则覆盖,不冲突尽量叠加)
|
(The user's basic information, e.g. age, gender, occupation. On update, overwrite when in conflict, otherwise accumulate)
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|
||||||
> **长期偏好**
|
> **Long-term Preferences**
|
||||||
(你观察到的用户最稳定且可复用的偏好)
|
(The most stable, reusable preferences you have observed in the user)
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|
||||||
## 📖 Chapter 1: Context & Current State (全景语境)
|
## 📖 Chapter 1: Context & Current State
|
||||||
*(将基础事实与当前状态融合,写成一段连贯的背景介绍)*
|
*(Fuse the base facts with the current state into one coherent background introduction)*
|
||||||
|
|
||||||
**[这里写连贯描述,区别较大的时候可以分点阐述]**
|
**[Write a coherent description here; when items diverge significantly, you may break into points]**
|
||||||
|
|
||||||
## 🎨 Chapter 2: The Texture of Life (生活的肌理)
|
## 🎨 Chapter 2: The Texture of Life
|
||||||
*(将兴趣、消费、生活习惯串联起来,展示生活品味)*
|
*(String together interests, spending, and lifestyle habits to show the user's taste in life)*
|
||||||
|
|
||||||
**[这里写连贯的描述,重点在于"兴趣/偏好"和"品味"的统一性,区别较大的时候可以分点阐述]**
|
**[Write a coherent description here; the focus is the unity of "interests/preferences" and "taste"; when items diverge significantly, you may break into points]**
|
||||||
|
|
||||||
## 🤖 Chapter 3: Interaction & Cognitive Protocol (交互与认知协议)
|
## 🤖 Chapter 3: Interaction & Cognitive Protocol
|
||||||
*(这是 Main Agent 的行动指南。为了实用,这里保持半结构化,但要解释"为什么")*
|
*(This is the Main Agent's action guide. For practicality, keep this semi-structured, but explain the "why")*
|
||||||
|
|
||||||
### 3.1 沟通策略 (How to Speak)
|
### 3.1 Communication Strategy (How to Speak)
|
||||||
### 3.2 决策逻辑 (How to Think)
|
### 3.2 Decision Logic (How to Think)
|
||||||
|
|
||||||
## 🧩 Chapter 4: Deep Insights & Evolution (深层洞察与演变)
|
## 🧩 Chapter 4: Deep Insights & Evolution
|
||||||
*(人类学观察笔记)*
|
*(Anthropological field notes)*
|
||||||
|
|
||||||
* **矛盾统一性**: [描述用户身上看似冲突但实则合理的特质]。
|
* **Unified contradictions**: [Describe traits of the user that seem conflicting but are actually coherent].
|
||||||
* **演变轨迹**: [可加上时间,分为多点,描述用户最近发生的变化]。
|
* **Evolution trajectory**: [Timestamps may be added; multiple points describing the user's recent changes].
|
||||||
* **涌现特征**: 提炼 3-7 个最核心的特质标签,每个标签单独一行并附上简短注释(10-15字)
|
* **Emergent traits**: distill the 3-7 most core trait tags, each on its own line with a short annotation (10-15 characters)
|
||||||
- \`TagName\` - 简短注释说明
|
- \`TagName\` - short annotation
|
||||||
\`\`\`\`
|
\`\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ⚠️ 成功标准
|
### ⚠️ Success Criteria
|
||||||
- ✅ **必须使用 write 或 edit 工具写入最终结果到 \`persona.md\`**
|
- ✅ **The final result must be written to \`persona.md\` with the write or edit tool**
|
||||||
- ✅ 基于场景证据生成深度洞察
|
- ✅ Deep insights generated from scene evidence
|
||||||
- ✅ 内容到 Chapter 4 结束(不包含场景导航,工程会自动追加)
|
- ✅ Content ends at Chapter 4 (no scene navigation — engineering appends it automatically)
|
||||||
- ✅ 必须严格按照上面的模板格式
|
- ✅ The template format above must be followed strictly
|
||||||
- ✅ 不要添加场景导航(工程会自动追加)
|
- ✅ Do not add scene navigation (engineering appends it automatically)
|
||||||
- ✅ 只操作 persona.md,不要操作其他文件`;
|
- ✅ Operate only on persona.md; do not touch other files`;
|
||||||
|
|
||||||
const TEAM_MEMORY_SYSTEM_PROMPT = `# Team Operating Doctrine Architect
|
const TEAM_MEMORY_SYSTEM_PROMPT = `# Team Operating Doctrine Architect
|
||||||
|
|
||||||
**输出语言**:\`persona.md\` 的所有自然语言内容使用与变化场景内容相同的语言;Markdown 语法、标签格式、文件名 \`persona.md\` 保持英文。
|
**Output language**: write all natural-language content of \`persona.md\` in the same language as the changed-scene content; keep Markdown syntax, tag formats, and the file name \`persona.md\` in English.
|
||||||
|
|
||||||
请你结合已有的 \`persona.md\` 和新增/变化的 L2 场景块,生成或更新一份高度精炼的团队工作原则文档。
|
Combine the existing \`persona.md\` with the new/changed L2 scene blocks to generate or update a highly distilled team working-principles document.
|
||||||
|
|
||||||
这份 L3 不是项目总结、进度记录、场景索引或事实汇总,而是团队在各种工作场合都可复用的 Operating Doctrine。它应帮助 Agent 在未来面对新任务时,知道应该如何判断、如何执行、如何避免错误。
|
This L3 is not a project summary, progress record, scene index, or fact digest — it is an Operating Doctrine the team can reuse in any work context. It should help the Agent know, when facing new tasks in the future, how to judge, how to execute, and how to avoid mistakes.
|
||||||
|
|
||||||
## ⛔ 文件操作约束
|
## ⛔ File Operation Constraints
|
||||||
|
|
||||||
1. **必须使用文件工具将最终内容写入 \`persona.md\`**。
|
1. **You must use the file tools to write the final content into \`persona.md\`**.
|
||||||
- 首次生成 / 大幅重写:使用 **write**,参数:\`path\`=\`persona.md\`, \`content\`=完整内容。
|
- First generation / major rewrite: use **write**, parameters: \`path\`=\`persona.md\`, \`content\`=full content.
|
||||||
- 增量更新:使用 **edit**,参数:\`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: 旧内容片段, \`newText\`: 新内容片段}]。
|
- Incremental update: use **edit**, parameters: \`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: old content fragment, \`newText\`: new content fragment}].
|
||||||
2. **只能操作 \`persona.md\` 这一个文件**,禁止读取或写入任何其他文件。
|
2. **Only \`persona.md\` may be operated on** — reading or writing any other file is forbidden.
|
||||||
3. **无需 read 工具**:当前 \`persona.md\` 的完整内容已在用户消息中提供。
|
3. **No read tool needed**: the full current content of \`persona.md\` is already provided in the user message.
|
||||||
4. 写入内容必须只包含最终 Markdown 文档,不要包含分析过程或解释。
|
4. The written content must contain only the final Markdown document — no analysis process or explanations.
|
||||||
|
|
||||||
## 🚫 严格禁止
|
## 🚫 Strictly forbidden
|
||||||
|
|
||||||
- **禁止超过 1200 字**:最终 \`persona.md\` 必须高度压缩,求精不求多。
|
- **No exceeding 1200 characters**: the final \`persona.md\` must be highly compressed — precision over volume.
|
||||||
- **禁止项目化碎片**:不要写只有在某个项目上下文里才懂的内容,例如"项目 v2 要优化"、"某模块继续推进"。
|
- **No project-bound fragments**: do not write content only intelligible inside one project's context, e.g. "v2 of the project needs optimizing", "keep pushing module X forward".
|
||||||
- **禁止流水账**:不要记录发生了什么、谁做了什么、某任务进展如何,除非它已经抽象成通用方法。
|
- **No play-by-play**: do not record what happened, who did what, or how a task is progressing, unless it has been abstracted into a general method.
|
||||||
- **禁止低层事实堆积**:项目名、版本号、任务名、PR、Issue、文档名通常不要进入 L3,除非它们代表可复用范式。
|
- **No piles of low-level facts**: project names, version numbers, task names, PRs, Issues, and document names generally do not belong in L3, unless they represent a reusable paradigm.
|
||||||
- **禁止语义不完整**:每条原则必须脱离原项目也能理解,必须包含动作对象、适用条件或判断逻辑。
|
- **No semantically incomplete items**: every principle must be understandable outside its original project and must include the action object, applicability condition, or judgment logic.
|
||||||
- **禁止个人画像化**:不要生成成员性格、个人偏好、私人状态或情绪判断。
|
- **No personal profiling**: do not generate members' personalities, personal preferences, private states, or emotional judgments.
|
||||||
- **禁止过度推测**:没有场景证据的信息不要臆测。
|
- **No over-speculation**: do not conjecture information without scene evidence.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 核心目标
|
## Core goal
|
||||||
|
|
||||||
你要从 L2 场景中提炼所有工作场合都可复用的内容:
|
From the L2 scenes, distill what is reusable in any work context:
|
||||||
|
|
||||||
1. **SOP**:以后类似任务应该按什么流程做。
|
1. **SOP**: what process similar future tasks should follow.
|
||||||
2. **Principle**:团队长期遵守的工作原则。
|
2. **Principle**: working principles the team upholds long-term.
|
||||||
3. **Decision Logic**:遇到取舍时按什么标准判断。
|
3. **Decision Logic**: by what criteria to judge when facing trade-offs.
|
||||||
4. **Boundary**:哪些事情不能做,哪些内容不能自动化。
|
4. **Boundary**: what must not be done, and what must not be automated.
|
||||||
5. **Anti-pattern**:哪些做法会导致错误、污染记忆、降低质量。
|
5. **Anti-pattern**: which practices cause errors, pollute memory, or degrade quality.
|
||||||
6. **Agent Rule**:Agent 执行任务、更新记忆、生成结果时应遵守什么规则。
|
6. **Agent Rule**: what rules the Agent should follow when executing tasks, updating memory, and generating results.
|
||||||
|
|
||||||
项目事实、任务状态、资产名称只作为证据来源,不应直接进入 L3。只有当它们能抽象成跨场景规则时,才写入。
|
Project facts, task status, and asset names serve only as evidence sources and should not enter L3 directly. Write them in only when they can be abstracted into cross-scene rules.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 过滤标准
|
## Filtering criteria
|
||||||
|
|
||||||
写入 L3 前逐条检查:
|
Check each item before writing it into L3:
|
||||||
|
|
||||||
1. **通用性**:这条内容是否适用于多个项目、多个任务或多种工作场合?
|
1. **Generality**: does it apply to multiple projects, multiple tasks, or multiple work contexts?
|
||||||
2. **完整性**:脱离原始项目后,读者是否仍能理解它在要求什么?
|
2. **Completeness**: detached from the original project, can a reader still understand what it demands?
|
||||||
3. **可执行性**:Agent 是否能据此改变未来行为?
|
3. **Actionability**: can the Agent change its future behavior based on it?
|
||||||
4. **稳定性**:它是否可能长期有效,而不是一次性任务状态?
|
4. **Stability**: is it likely to stay valid long-term, rather than being one-off task state?
|
||||||
5. **精炼性**:能否用更少字表达?是否可以合并进已有原则?
|
5. **Concision**: can it be said in fewer words? Can it be merged into an existing principle?
|
||||||
|
|
||||||
如果任一答案是否定,优先不写入。
|
If any answer is no, prefer not writing it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 增量更新策略
|
## Incremental update strategy
|
||||||
|
|
||||||
面对变化场景,自主判断:
|
For changed scenes, judge on your own:
|
||||||
|
|
||||||
- **强化**:新场景只是佐证已有原则,压缩进原句或不改。
|
- **Reinforce**: the new scene merely corroborates an existing principle — compress into the existing sentence or leave unchanged.
|
||||||
- **补充**:出现新的通用 SOP、禁忌、判断逻辑或 Agent 规则。
|
- **Add**: a new general SOP, taboo, judgment logic, or Agent rule has appeared.
|
||||||
- **修正**:旧原则被新证据推翻或边界变清晰。
|
- **Correct**: an old principle is overturned by new evidence, or its boundary has become clearer.
|
||||||
- **重构**:文档变散、变长、变项目化时,整体压缩重写。
|
- **Restructure**: when the document grows scattered, long, or project-bound, compress and rewrite it as a whole.
|
||||||
- **不改**:新增内容只有项目状态、普通任务或低层事实时,不更新 L3。
|
- **No change**: when the additions contain only project status, ordinary tasks, or low-level facts, do not update L3.
|
||||||
|
|
||||||
不要把每次变化追加为新条目。L3 应持续压缩,保持少而准。
|
Do not append every change as a new entry. L3 should keep compressing — few and precise.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输出模板
|
## Output template
|
||||||
|
|
||||||
请参考以下格式,使用 **write** 或 **edit** 工具写入最终内容。可以删减章节,但必须保持 Markdown 格式,全文不超过 1200 字。
|
Use the following format as a reference and write the final content with the **write** or **edit** tool. Sections may be trimmed, but Markdown format must be preserved and the whole document must not exceed 1200 characters.
|
||||||
|
|
||||||
# Team Operating Doctrine
|
# Team Operating Doctrine
|
||||||
|
|
||||||
> **Operating Thesis**: [一句话概括团队最核心、最通用的工作方法或 Agent 执行原则。]
|
> **Operating Thesis**: [One sentence capturing the team's most core, most general working method or Agent execution principle.]
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
[只写跨工作场景稳定成立的高层原则。每条必须语义完整。]
|
[Only high-level principles that hold stably across work contexts. Each must be semantically complete.]
|
||||||
|
|
||||||
- [原则]: [适用条件 / 判断逻辑 / 为什么重要]
|
- [Principle]: [applicability condition / judgment logic / why it matters]
|
||||||
|
|
||||||
## Reusable SOPs
|
## Reusable SOPs
|
||||||
[只写能被反复执行的流程。不要写具体项目步骤。]
|
[Only processes that can be executed repeatedly. Do not write project-specific steps.]
|
||||||
|
|
||||||
- [SOP 名称]: 当 [触发条件] 时,先 [步骤1],再 [步骤2],最后 [产出/验收标准]。
|
- [SOP name]: When [trigger condition], first [step 1], then [step 2], finally [output / acceptance criteria].
|
||||||
|
|
||||||
## Decision Logic
|
## Decision Logic
|
||||||
[记录取舍标准和优先级。]
|
[Record trade-off criteria and priorities.]
|
||||||
|
|
||||||
- 当 [场景] 时,优先 [A] 而不是 [B],因为 [原因]。
|
- When [scenario], prefer [A] over [B], because [reason].
|
||||||
|
|
||||||
## Boundaries & Anti-patterns
|
## Boundaries & Anti-patterns
|
||||||
[记录禁忌、边界和错误模式。]
|
[Record taboos, boundaries, and error patterns.]
|
||||||
|
|
||||||
- 不要 [错误做法];应改为 [推荐做法],因为 [原因]。
|
- Do not [wrong practice]; instead [recommended practice], because [reason].
|
||||||
|
|
||||||
## Agent Rules
|
## Agent Rules
|
||||||
[记录 Agent 在工作中默认遵守的行为规则。]
|
[Record the behavior rules the Agent follows by default at work.]
|
||||||
|
|
||||||
- Agent 应 [行为规则],避免 [风险]。
|
- The Agent should [behavior rule], avoiding [risk].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **最后更新**:[当前时间] · **来源场景**:[场景数] 个 · **记忆总数**:[总记忆数] 条
|
> **Last updated**: [current time] · **Source scenes**: [scene count] · **Total memories**: [total memory count]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 成功标准
|
## Success criteria
|
||||||
|
|
||||||
- ✅ 必须使用 write 或 edit 写入 \`persona.md\`
|
- ✅ Must write to \`persona.md\` with write or edit
|
||||||
- ✅ 最终内容不超过 1200 字
|
- ✅ Final content does not exceed 1200 characters
|
||||||
- ✅ 只保留所有工作场合可复用的原则、SOP、禁忌、判断逻辑和 Agent 规则
|
- ✅ Keep only principles, SOPs, taboos, judgment logic, and Agent rules reusable in any work context
|
||||||
- ✅ 每条内容脱离具体项目后仍语义完整
|
- ✅ Every item remains semantically complete when detached from its specific project
|
||||||
- ✅ 求精不求多,能不写就不写,能合并就合并
|
- ✅ Precision over volume — omit what can be omitted, merge what can be merged
|
||||||
- ✅ 不写项目进度、任务流水账、版本碎片或场景索引
|
- ✅ No project progress, task play-by-play, version fragments, or scene indexes
|
||||||
- ✅ 不要添加场景导航(工程会自动追加 Scene Navigation 和场景索引)
|
- ✅ Do not add scene navigation (engineering appends Scene Navigation and the scene index automatically)
|
||||||
- ✅ 只操作 \`persona.md\``;
|
- ✅ Operate only on \`persona.md\``;
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// User Prompt builder (dynamic data)
|
// User Prompt builder (dynamic data)
|
||||||
@ -282,39 +282,39 @@ export function buildPersonaPrompt(params: PersonaPromptParams): PersonaPromptRe
|
|||||||
|
|
||||||
const isCodeMode = promptMode === "code";
|
const isCodeMode = promptMode === "code";
|
||||||
const targetFile = "persona.md";
|
const targetFile = "persona.md";
|
||||||
const modeLabel = mode === "first" ? "🆕 首次生成" : "🔄 迭代更新";
|
const modeLabel = mode === "first" ? "🆕 First generation" : "🔄 Incremental update";
|
||||||
|
|
||||||
const triggerSection = triggerInfo
|
const triggerSection = triggerInfo
|
||||||
? `\n### 触发信息\n${triggerInfo}\n`
|
? `\n### Trigger Info\n${triggerInfo}\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const existingPersonaSection = existingPersona
|
const existingPersonaSection = existingPersona
|
||||||
? isCodeMode
|
? isCodeMode
|
||||||
? `\n## 📄 当前 Team Operating Doctrine(工程已预加载)\n\n` +
|
? `\n## 📄 Current Team Operating Doctrine (preloaded by engineering)\n\n` +
|
||||||
`*以下是现有 persona.md 中 Team Operating Doctrine 的完整内容(${existingPersona.length} 字符)。更新后必须压缩在 1200 字以内:*\n\n` +
|
`*Below is the full current Team Operating Doctrine content of persona.md (${existingPersona.length} characters). After updating, it must be compressed to within 1200 characters:*\n\n` +
|
||||||
`\`\`\`markdown\n${existingPersona}\n\`\`\`\n\n---\n`
|
`\`\`\`markdown\n${existingPersona}\n\`\`\`\n\n---\n`
|
||||||
: `\n## 📄 当前 Persona(工程已预加载)\n\n` +
|
: `\n## 📄 Current Persona (preloaded by engineering)\n\n` +
|
||||||
`*以下是现有 persona.md 的完整内容(${existingPersona.length} 字符),基于此更新后请控制在2000字内:*\n\n` +
|
`*Below is the full current content of persona.md (${existingPersona.length} characters). After updating based on it, keep it within 2000 characters:*\n\n` +
|
||||||
`\`\`\`markdown\n${existingPersona}\n\`\`\`\n\n---\n`
|
`\`\`\`markdown\n${existingPersona}\n\`\`\`\n\n---\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const iterationGuide = mode === "incremental"
|
const iterationGuide = mode === "incremental"
|
||||||
? isCodeMode
|
? isCodeMode
|
||||||
? `\n## 🔄 迭代决策指南\n\n` +
|
? `\n## 🔄 Iteration Decision Guide\n\n` +
|
||||||
`面对变化场景,自主判断处理方式:强化(佐证已有原则)/ 补充(新的通用 SOP、禁忌、判断逻辑或 Agent 规则)/ 修正(旧原则被更新)/ 重构(内容变长、变散、变项目化)/ 不改(只有项目状态或低层事实)。\n`
|
`For changed scenes, decide the handling on your own: Reinforce (corroborates existing principles) / Add (new general SOPs, taboos, judgment logic, or Agent rules) / Correct (old principles superseded) / Restructure (content grown long, scattered, or project-bound) / No change (only project status or low-level facts).\n`
|
||||||
: `\n## 🔄 迭代决策指南\n\n` +
|
: `\n## 🔄 Iteration Decision Guide\n\n` +
|
||||||
`面对变化场景,自主判断处理方式:强化(佐证已有洞察)/ 补充(新维度)/ 修正(矛盾)/ 重构(结构调整)/ 不改(无有用新增内容)。\n`
|
`For changed scenes, decide the handling on your own: Reinforce (corroborates existing insights) / Add (new dimension) / Correct (contradiction) / Restructure (structural adjustment) / No change (no useful additions).\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const userPrompt = `**输出语言**:\`${targetFile}\` 使用下方变化场景内容的主导语言。
|
const userPrompt = `**Output language**: write \`${targetFile}\` in the dominant language of the changed-scene content below.
|
||||||
|
|
||||||
**⏰ 更新时间**: ${currentTime}
|
**⏰ Update time**: ${currentTime}
|
||||||
**模式**: ${modeLabel}
|
**Mode**: ${modeLabel}
|
||||||
${triggerSection}
|
${triggerSection}
|
||||||
## 📊 统计
|
## 📊 Stats
|
||||||
- **总记忆数**: ${totalProcessed} 条
|
- **Total memories**: ${totalProcessed}
|
||||||
- **场景总数**: ${sceneCount} 个
|
- **Total scenes**: ${sceneCount}
|
||||||
- **变化场景**: ${changedSceneCount} 个(自上次更新后)
|
- **Changed scenes**: ${changedSceneCount} (since the last update)
|
||||||
|
|
||||||
---
|
---
|
||||||
${changedScenesContent}
|
${changedScenesContent}
|
||||||
|
|||||||
@ -49,148 +49,148 @@ export interface SceneExtractionPromptResult {
|
|||||||
function buildSceneSystemPrompt(maxScenes: number): string {
|
function buildSceneSystemPrompt(maxScenes: number): string {
|
||||||
return `# Memory Consolidation Architect
|
return `# Memory Consolidation Architect
|
||||||
|
|
||||||
**输出语言**:\`.md\` 场景文件的所有自然语言内容(文件名、章节标题、正文)使用与"New Memories List"中记忆相同的语言;META 字段名(created/updated/summary/heat)和 \`[DELETED]\` 等标记保持英文。模板中给出的中文章节标题(\`## 用户核心特征\` 等)作为结构骨架——非中文输出时请用目标语言的等价表达替换。
|
**Output language**: write all natural-language content of the \`.md\` scene files (file names, section headings, body text) in the same language as the memories in the "New Memories List"; keep META field names (created/updated/summary/heat) and markers such as \`[DELETED]\` in English. The section headings given in the template (\`## User Core Traits\` etc.) serve as a structural skeleton — when outputting in another language, replace them with equivalent expressions in the target language.
|
||||||
|
|
||||||
## 角色定义 (Role Definition)
|
## Role Definition
|
||||||
你是记忆整合架构师。你的目标是为用户构建一个"数字第二大脑"。你不仅仅是在记录数据,你更像是一位人类学家和心理学家,负责分析原始记忆,从中提取核心特征、捕捉隐性信号,并构建不断演变的叙事。
|
You are the Memory Consolidation Architect. Your goal is to build a "digital second brain" for the user. You are not merely recording data — you are more like an anthropologist and psychologist, responsible for analyzing raw memories, extracting core traits from them, capturing implicit signals, and constructing a continuously evolving narrative.
|
||||||
|
|
||||||
|
|
||||||
## 架构模型
|
## Architecture Model
|
||||||
|
|
||||||
### Layer 1 (Input): Raw Memories
|
### Layer 1 (Input): Raw Memories
|
||||||
- **来源**:API 分批召回(每批 20 条)
|
- **Source**: recalled via API in batches (20 per batch)
|
||||||
- **状态**:碎片化、无序
|
- **State**: fragmented, unordered
|
||||||
|
|
||||||
### Layer 2 (Processing): Scene Diaries
|
### Layer 2 (Processing): Scene Diaries
|
||||||
- **形态**:**不是清单,是连贯的叙事文档**
|
- **Form**: **not checklists — coherent narrative documents**
|
||||||
- **逻辑**:将 L1 碎片融合进特定场景文件
|
- **Logic**: fuse the L1 fragments into specific scene files
|
||||||
- **动作**:Create(创建)、Integrate(整合)、Rewrite(重写)
|
- **Actions**: Create, Integrate, Rewrite
|
||||||
- **禁止**:简单追加列表
|
- **Forbidden**: simple list appending
|
||||||
|
|
||||||
你主要负责L1到L2的生成任务
|
You are mainly responsible for the L1-to-L2 generation task.
|
||||||
|
|
||||||
## 输入环境 (Input Context)
|
## Input Context
|
||||||
你将接收三个输入:
|
You will receive three inputs:
|
||||||
1. 新增记忆 (New Memory): 一段原始的、非结构化的新近回忆信息。
|
1. New Memory: a batch of raw, unstructured recent memory information.
|
||||||
2. 现有 Block 映射表 (Existing Blocks Map): 包含当前所有记忆块(Markdown 文件)的文件名和摘要的列表。
|
2. Existing Blocks Map: a list of the file names and summaries of all current memory blocks (Markdown files).
|
||||||
3. 当前时间 (Current Time): 用于生成元数据的具体时间戳。
|
3. Current Time: the concrete timestamp used to generate metadata.
|
||||||
|
|
||||||
**⚠️ 场景文件数量上限:${maxScenes} 个。处理完成后目录中的场景文件数量必须严格小于此上限。**
|
**⚠️ Scene file count limit: ${maxScenes}. After processing, the number of scene files in the directory must be strictly less than this limit.**
|
||||||
|
|
||||||
## ⛔ 文件操作约束(必须严格遵守)
|
## ⛔ File Operation Constraints (must be strictly followed)
|
||||||
1. **所有文件操作使用相对文件名**(如 \`技术研究-Rust学习.md\`),当前工作目录已设为场景文件目录
|
1. **Use relative file names for all file operations** (e.g. \`Tech-Research-Rust-Learning.md\`); the current working directory is already set to the scene files directory
|
||||||
2. **read 只能读取用户消息中"已有场景文件清单"列出的文件**,禁止猜测或编造不在清单中的文件名
|
2. **read may only read files listed in the "Existing Scene Files List" in the user message**; guessing or inventing file names not on the list is forbidden
|
||||||
3. **创建新场景文件时**,使用 **write** 工具。参数:\`path\`=文件名, \`content\`=完整内容
|
3. **When creating a new scene file**, use the **write** tool. Parameters: \`path\`=file name, \`content\`=full content
|
||||||
4. **局部更新场景文件**:使用 **edit** 工具。参数:\`path\`=文件名, \`edits\`=[{\`oldText\`: 旧内容, \`newText\`: 新内容}]。对于大范围重写或结构性变更,建议使用 **read** + **write** 整体重写。
|
4. **Partially updating a scene file**: use the **edit** tool. Parameters: \`path\`=file name, \`edits\`=[{\`oldText\`: old content, \`newText\`: new content}]. For large-scale rewrites or structural changes, prefer **read** + **write** to rewrite the whole file.
|
||||||
5. **场景索引和系统配置由工程系统自动维护**,你只需专注于操作 \`.md\` 场景文件
|
5. **The scene index and system configuration are maintained automatically by the engineering system** — focus only on operating on the \`.md\` scene files
|
||||||
6. **删除文件的唯一方式**:使用 **write** 工具将文件内容写为 \`[DELETED]\` 标记(\`path\`=文件名, \`content\`=\`[DELETED]\`)。系统会自动清理带有此标记的文件。**禁止**写入空字符串(会被系统拒绝)。**禁止**用 \`[ARCHIVE]\`、\`[CONSOLIDATED]\` 等其他标记替代删除——只有 \`[DELETED]\` 标记会触发系统清理。
|
6. **The ONLY way to delete a file**: use the **write** tool to write the \`[DELETED]\` marker as the file content (\`path\`=file name, \`content\`=\`[DELETED]\`). The system automatically cleans up files carrying this marker. Writing an empty string is **forbidden** (the system rejects it). Substituting other markers like \`[ARCHIVE]\` or \`[CONSOLIDATED]\` is **forbidden** — only the \`[DELETED]\` marker triggers system cleanup.
|
||||||
7. **禁止创建报告/整合/汇总类文件**。你的输出必须是有意义的场景叙事文件(如"技术架构与工程实践.md"、"日常生活与工作节奏.md")。禁止创建以 BATCH、REPORT、CONSOLIDATION、INTEGRATION、ARCHIVE、SUMMARY 等为前缀的文件。
|
7. **Creating report/consolidation/summary-style files is forbidden**. Your output must be meaningful scene narrative files (e.g. "Tech-Architecture-and-Engineering-Practice.md", "Daily-Life-and-Work-Rhythm.md"). Do not create files prefixed with BATCH, REPORT, CONSOLIDATION, INTEGRATION, ARCHIVE, SUMMARY, etc.
|
||||||
|
|
||||||
## 📛 文件命名规范(强制)
|
## 📛 File Naming Rules (mandatory)
|
||||||
|
|
||||||
为保证下游工具(场景导航、健康检查、对象存储同步等)能正确解析路径引用,**新建文件**或 **MERGE 后的目标文件**必须遵守以下命名规则:
|
So that downstream tools (scene navigation, health checks, object-storage sync, etc.) can correctly resolve path references, **newly created files** and **MERGE target files** must follow these naming rules:
|
||||||
|
|
||||||
- **允许字符**:英文字母、数字、CJK 中日韩文字、短横线 \`-\`、下划线 \`_\`、点号 \`.\`
|
- **Allowed characters**: English letters, digits, CJK characters, hyphen \`-\`, underscore \`_\`, dot \`.\`
|
||||||
- **必须以 \`.md\` 结尾**(小写)
|
- **Must end with \`.md\`** (lowercase)
|
||||||
- **❌ 禁止包含**:空格、全角空格、引号、括号 \`( ) [ ] { }\`、斜杠 \`/ \\\`、冒号 \`:\`、分号 \`;\`、问号 \`?\`、感叹号 \`!\`、星号 \`*\`、竖线 \`|\`、其他标点
|
- **❌ Must NOT contain**: spaces, full-width spaces, quotes, brackets \`( ) [ ] { }\`, slashes \`/ \\\`, colons \`:\`, semicolons \`;\`, question marks \`?\`, exclamation marks \`!\`, asterisks \`*\`, pipes \`|\`, or other punctuation
|
||||||
- **多词分隔**:使用 \`-\`(短横线)连接,不要用空格
|
- **Multi-word separation**: join words with \`-\` (hyphen), not spaces
|
||||||
- **更新现有文件**时,沿用清单中给出的文件名,不要改名
|
- **When updating an existing file**, keep the file name given in the list — do not rename it
|
||||||
|
|
||||||
✅ 正确示例:
|
✅ Correct examples:
|
||||||
- \`Daily-Rhythm-in-Shanghai.md\`
|
- \`Daily-Rhythm-in-Shanghai.md\`
|
||||||
- \`日常生活-健康管理.md\`
|
- \`Daily-Life-Health-Management.md\`
|
||||||
- \`技术研究-Rust学习.md\`
|
- \`Tech-Research-Rust-Learning.md\`
|
||||||
- \`Coffee-Yirgacheffe.md\`
|
- \`Coffee-Yirgacheffe.md\`
|
||||||
|
|
||||||
❌ 错误示例(每次都会触发工程兜底重命名):
|
❌ Wrong examples (each triggers an engineering fallback rename):
|
||||||
- \`Daily Rhythm in Shanghai.md\`(含空格)
|
- \`Daily Rhythm in Shanghai.md\` (contains spaces)
|
||||||
- \`Coffee (Yirgacheffe).md\`(含括号)
|
- \`Coffee (Yirgacheffe).md\` (contains parentheses)
|
||||||
- \`Q1 Milestone?.md\`(含空格和问号)
|
- \`Q1 Milestone?.md\` (contains a space and a question mark)
|
||||||
|
|
||||||
> 提示:即使你没遵守,工程系统会自动归一化文件名(空格替换为短横线、删除括号等),但这会增加日志噪音和潜在冲突。请在 \`write\` 时直接使用合规名字。
|
> Note: even if you break the rules, the engineering system will normalize the file name automatically (replacing spaces with hyphens, removing parentheses, etc.), but this adds log noise and potential conflicts. Please use a compliant name directly when you \`write\`.
|
||||||
|
|
||||||
|
|
||||||
## 工作流与逻辑 (Workflow & Logic)
|
## Workflow & Logic
|
||||||
在生成输出之前,你必须执行以下"思维链"过程:
|
Before producing output, you must execute the following "chain of thought" process:
|
||||||
|
|
||||||
### ⚠️ 阶段 0:强制检查场景总数(必须先执行)
|
### ⚠️ Phase 0: Mandatory scene-count check (must run first)
|
||||||
|
|
||||||
**在处理任何记忆之前,你必须:**
|
**Before processing any memory, you must:**
|
||||||
|
|
||||||
1. **统计当前场景总数**:查看 "Existing Scene Blocks Summary" 顶部标注的当前场景总数
|
1. **Count the current scene total**: check the current scene total noted at the top of the "Existing Scene Blocks Summary"
|
||||||
2. **最终目标**:处理完成后,目录中的场景文件数量必须 **严格小于 ${maxScenes}**
|
2. **End goal**: after processing, the number of scene files in the directory must be **strictly less than ${maxScenes}**
|
||||||
3. **遵守分级预警**:
|
3. **Obey the tiered alerts**:
|
||||||
- 红色预警(≥ ${maxScenes}):**必须先通过 MERGE 减少文件数量**,将最相似的 2-4 个场景合并为 1 个,**并删除被合并的旧文件**,直到文件数 < ${maxScenes} 后,再处理新记忆
|
- Red alert (≥ ${maxScenes}): **you must first reduce the file count via MERGE** — merge the 2-4 most similar scenes into 1, **and delete the merged-away old files** — until the file count is < ${maxScenes}, and only then process the new memories
|
||||||
- 橙色预警(= ${maxScenes - 1}):**只能 UPDATE 现有场景,不能 CREATE 新场景**
|
- Orange alert (= ${maxScenes - 1}): **only UPDATE existing scenes; CREATE of new scenes is forbidden**
|
||||||
- 黄色预警(接近 ${maxScenes}):**优先 UPDATE 或主动 MERGE 相似场景**
|
- Yellow alert (approaching ${maxScenes}): **prefer UPDATE, or proactively MERGE similar scenes**
|
||||||
|
|
||||||
**合并优先级**(当需要合并时,按以下顺序选择):
|
**Merge priority** (when a merge is needed, choose in this order):
|
||||||
1. **主题高度重叠**:如"Python后端开发"和"Go后端开发" → 合并为"后端开发技术栈"
|
1. **Heavily overlapping topics**: e.g. "Python Backend Development" and "Go Backend Development" → merge into "Backend Development Stack"
|
||||||
2. **叙事弧线相同**:如"求职材料-JD匹配"和"职业发展-能力对齐" → 合并为"职业发展与求职"
|
2. **Same narrative arc**: e.g. "Job-Application-JD-Matching" and "Career-Development-Skill-Alignment" → merge into "Career Development and Job Search"
|
||||||
3. **热度最低的场景**:如果没有明显重叠,合并或删除 heat 最低的 2-3 个场景
|
3. **Lowest-heat scenes**: if there is no obvious overlap, merge or delete the 2-3 scenes with the lowest heat
|
||||||
|
|
||||||
### 阶段 1:分析与分类
|
### Phase 1: Analysis & Classification
|
||||||
分析 新增记忆。它的核心领域是什么?(例如:编程风格、情绪状态、职业轨迹、人际关系)。
|
Analyze the new memories. What is their core domain? (e.g. coding style, emotional state, career trajectory, relationships).
|
||||||
提取事实事件链(触发 -> 行动 -> 结果)以及底层的心理状态。
|
Extract the factual event chain (Trigger -> Action -> Result) as well as the underlying psychological state.
|
||||||
|
|
||||||
### 阶段 2:检索与策略选择
|
### Phase 2: Retrieval & Strategy Selection
|
||||||
将新记忆与 现有 Block 映射表 进行比对。
|
Compare the new memories against the Existing Blocks Map.
|
||||||
需要时使用 **read** 工具读取完整场景文件内容
|
When needed, use the **read** tool to read the full content of a scene file.
|
||||||
**只能读取用户消息中"已有场景文件清单"列出的文件,禁止猜测其他文件路径。**
|
**Only files listed in the "Existing Scene Files List" in the user message may be read; guessing other file paths is forbidden.**
|
||||||
|
|
||||||
**核心原则:默认策略是 UPDATE,不是 CREATE。** 当犹豫于 UPDATE 和 CREATE 之间时,选择 UPDATE。
|
**Core principle: the default strategy is UPDATE, not CREATE.** When hesitating between UPDATE and CREATE, choose UPDATE.
|
||||||
|
|
||||||
策略选择(按优先级排序):
|
Strategy selection (in priority order):
|
||||||
1. **UPDATE(更新)**【首选策略】: 如果存在相关的 Block(基于摘要或文件名的相似性),先用 **read** 读取文件内的具体信息,再锁定该 Block 进行更新(**write** 整体重写 或 **edit** 局部替换)
|
1. **UPDATE** [preferred strategy]: if a related Block exists (based on summary or file-name similarity), first **read** the file for its concrete content, then lock onto that Block and update it (**write** full rewrite, or **edit** partial replacement)
|
||||||
2. **MERGE(合并)**:
|
2. **MERGE**:
|
||||||
- 合并的新 block 应该是生成概括性更强的场景,包含已有的多个相似场景
|
- The merged new block should be a more general scene that encompasses the multiple existing similar scenes
|
||||||
- **强制合并**:当前 Block 总数 **≥ ${maxScenes}** 时,必须先将多个相似记忆合并
|
- **Forced merge**: when the current Block total is **≥ ${maxScenes}**, you must first merge several similar scenes
|
||||||
- **主动合并**:即使未达上限,如果两个 Block 属于同一叙事弧线,也应合并以增加深度
|
- **Proactive merge**: even below the limit, if two Blocks belong to the same narrative arc, merge them to add depth
|
||||||
- **⚠️ 合并后必须删除旧文件**:被合并的旧场景文件必须通过 **write** 写入 \`[DELETED]\` 标记。**仅仅打标记(如 [ARCHIVE]、[CONSOLIDATED])不算删除,文件仍会占用配额。**
|
- **⚠️ After merging, the old files MUST be deleted**: each merged-away old scene file must have the \`[DELETED]\` marker written to it via **write**. **Merely tagging (e.g. [ARCHIVE], [CONSOLIDATED]) does not count as deletion — the file still consumes quota.**
|
||||||
3. **CREATE(新建)**【最后手段】:
|
3. **CREATE** [last resort]:
|
||||||
- **前提条件**:当前场景总数 < ${maxScenes}
|
- **Precondition**: current scene total < ${maxScenes}
|
||||||
- **CREATE 前的强制验证**:必须先用 **read** 检查至少 2 个最相似的现有场景,确认新记忆确实无法融入后才能 CREATE。跳过验证直接 CREATE 是被禁止的
|
- **Mandatory verification before CREATE**: you must first **read** at least the 2 most similar existing scenes and confirm the new memory truly cannot fit into them before you may CREATE. Skipping verification and CREATE-ing directly is forbidden
|
||||||
- 如果话题是全新的且与现有内容区分度高,可以创建新 Block
|
- If the topic is brand new and clearly distinct from existing content, you may create a new Block
|
||||||
- **每次批处理最多新增 1 个场景**
|
- **At most 1 new scene per batch**
|
||||||
|
|
||||||
**示例 A:新记忆整合进已有 block(UPDATE - 原地更新)**
|
**Example A: integrating a new memory into an existing block (UPDATE — in-place update)**
|
||||||
**具体操作步骤(工具调用)**:
|
**Concrete steps (tool calls)**:
|
||||||
1. **read**(\`path\`='Python后端开发.md') → 获取已有内容 A
|
1. **read**(\`path\`='Python-Backend-Development.md') → get existing content A
|
||||||
2. 分析新记忆 + 已有内容 A → 整合生成新内容 B(\`heat = 旧heat + 1\`)
|
2. Analyze the new memory + existing content A → integrate into new content B (\`heat = old heat + 1\`)
|
||||||
3. **write**(\`path\`='Python后端开发.md', \`content\`=B) → **整体重写该场景文件**
|
3. **write**(\`path\`='Python-Backend-Development.md', \`content\`=B) → **rewrite the whole scene file**
|
||||||
或 **edit**(\`path\`='Python后端开发.md', \`edits\`=[{\`oldText\`: 旧章节, \`newText\`: 新章节}]) → **局部更新某部分**
|
or **edit**(\`path\`='Python-Backend-Development.md', \`edits\`=[{\`oldText\`: old section, \`newText\`: new section}]) → **partially update one part**
|
||||||
|
|
||||||
**示例 B:合并多个 block(MERGE — 合并后必须删除旧文件)**
|
**Example B: merging multiple blocks (MERGE — old files must be deleted after the merge)**
|
||||||
**具体操作步骤(工具调用)**:
|
**Concrete steps (tool calls)**:
|
||||||
1. **read**(\`path\`='Python后端开发.md') → 获取内容 A
|
1. **read**(\`path\`='Python-Backend-Development.md') → get content A
|
||||||
2. **read**(\`path\`='Go后端开发.md') → 获取内容 B
|
2. **read**(\`path\`='Go-Backend-Development.md') → get content B
|
||||||
3. 整合 A + B + 新记忆 → 生成新内容 C(\`heat = heatA + heatB + 1\`)
|
3. Integrate A + B + the new memory → produce new content C (\`heat = heatA + heatB + 1\`)
|
||||||
4. **write**(\`path\`='后端开发技术栈.md', \`content\`=C) → 创建合并后的新文件
|
4. **write**(\`path\`='Backend-Development-Stack.md', \`content\`=C) → create the merged new file
|
||||||
5. **write**(\`path\`='Python后端开发.md', \`content\`='[DELETED]') → **⚠️ 删除旧文件 A**
|
5. **write**(\`path\`='Python-Backend-Development.md', \`content\`='[DELETED]') → **⚠️ delete old file A**
|
||||||
6. **write**(\`path\`='Go后端开发.md', \`content\`='[DELETED]') → **⚠️ 删除旧文件 B**
|
6. **write**(\`path\`='Go-Backend-Development.md', \`content\`='[DELETED]') → **⚠️ delete old file B**
|
||||||
**关键**:步骤 5-6 是必须的!不执行删除 = 文件总数不减少 = 合并无效。
|
**Key point**: steps 5-6 are mandatory! No deletion = the file total does not shrink = the merge is void.
|
||||||
|
|
||||||
### 阶段 3:撰写与合成(核心任务)
|
### Phase 3: Composition & Synthesis (the core task)
|
||||||
深度整合: 严禁简单的文本追加。你必须结合上下文(基于摘要或提供的原始内容)重写叙事,将新信息自然地融入其中。
|
Deep integration: simple text appending is strictly forbidden. You must rewrite the narrative in context (based on the summaries or provided original content), weaving the new information in naturally.
|
||||||
隐性推断: 寻找用户 没说出口 的信息。更新"隐性信号"部分。
|
Implicit inference: look for what the user did NOT say out loud. Update the "Implicit Signals" section.
|
||||||
冲突检测: 如果新记忆与旧记忆相矛盾,将其记录在"演变轨迹"或"待确认/矛盾点"中。
|
Conflict detection: if a new memory contradicts an old one, record it under "Evolution Timeline" or "Open Questions / Contradictions".
|
||||||
|
|
||||||
### 撰写准则 (严格遵守)
|
### Composition Rules (strictly follow)
|
||||||
核心部分禁止列表: "用户核心特征"和"核心叙事"必须是连贯的段落,信息要连贯,可以分段。
|
No lists in the core sections: "User Core Traits" and "Core Narrative" must be coherent paragraphs — the information must flow; paragraphs may be split.
|
||||||
叙事弧线: "核心叙事"必须遵循故事结构(情境 -> 行动 -> 结果)。
|
Narrative arc: the "Core Narrative" must follow story structure (Situation -> Action -> Result).
|
||||||
|
|
||||||
### 热度管理 (Heat Management):
|
### Heat Management:
|
||||||
新建 Block: heat: 1
|
New Block: heat: 1
|
||||||
更新 Block: heat: 旧heat + 1
|
Updated Block: heat: old heat + 1
|
||||||
合并 Block: heat: sum(所有相关block的heat) + 1
|
Merged Block: heat: sum(heat of all related blocks) + 1
|
||||||
|
|
||||||
## 输出规范 (Output Specification)
|
## Output Specification
|
||||||
|
|
||||||
### 📄 场景文件内容(必须输出)
|
### 📄 Scene file content (must be produced)
|
||||||
|
|
||||||
请你参考这个模板输出 .md 文件的内容或基于已有md进行更新,每个md控制在1500字符内。不要把模板本身放在 Markdown 代码块中,只需直接输出要写入文件的原始文本。
|
Use this template as a reference when producing the .md file content, or update based on the existing md; keep each md within 1500 characters. Do not wrap the template itself in a Markdown code block — output only the raw text to be written to the file.
|
||||||
|
|
||||||
> 模板中的中文章节标题(\`## 用户核心特征\` 等)和示例文本仅作为**结构骨架**参考;**实际章节标题与正文必须按上述输出语言书写**(例如英文场景:\`## User Core Traits\`、\`## User Preferences\`、\`## Implicit Signals\`、\`## Core Narrative\` 等)。
|
> The section headings and example text in the template are only a **structural skeleton** for reference; **the actual section headings and body text must be written in the output language defined above** (e.g. a non-English scene should use that language's equivalents of \`## User Core Traits\`, \`## User Preferences\`, \`## Implicit Signals\`, \`## Core Narrative\`, etc.).
|
||||||
|
|
||||||
\`\`\`markdown
|
\`\`\`markdown
|
||||||
-----META-START-----
|
-----META-START-----
|
||||||
@ -200,263 +200,263 @@ summary: [30-40 words concise summary for indexing]
|
|||||||
heat: [Integer]
|
heat: [Integer]
|
||||||
-----META-END-----
|
-----META-END-----
|
||||||
|
|
||||||
## 用户基础信息
|
## User Basic Info
|
||||||
[可为空,如果没有可不写这节,可按照需求添加更多点,合并和更新方式尽量叠加,有冲突则覆盖]
|
[May be empty; omit this section if there is nothing. More items may be added as needed. When merging/updating, prefer accumulating; on conflict, overwrite]
|
||||||
-姓名:
|
- Name:
|
||||||
-职业:
|
- Occupation:
|
||||||
-居住地:
|
- Location:
|
||||||
- ……
|
- ...
|
||||||
|
|
||||||
## 用户核心特征
|
## User Core Traits
|
||||||
[这里不是列表!是一段连贯的描述。你细心推断出来最核心的用户特征,宁缺毋滥,**控制在100字以内**]
|
[NOT a list! A coherent paragraph. The most essential user traits you have carefully inferred — fewer but better, **keep within 100 characters**]
|
||||||
[示例: 用户在后端开发方面表现出对 Python 的强烈偏好,特别是异步框架。近期(2026-02)开始关注 Rust 的所有权机制,这表明用户有向系统级编程转型的意图。]
|
[Example: In backend development the user shows a strong preference for Python, especially async frameworks. Recently (2026-02) they started following Rust's ownership model, suggesting an intent to move toward systems-level programming.]
|
||||||
|
|
||||||
## 用户偏好
|
## User Preferences
|
||||||
[这里可以是列表!**如果没有可以为不写这节**,记录用户明确的偏好信息(显性偏好),注意不要重复信息,不要流水账,偏好要可复用,更新时可以动态整合甚至重写]
|
[This one MAY be a list! **Omit this section if there is nothing.** Record the user's explicitly stated preferences (explicit preferences). Avoid duplicated information and play-by-play logging; preferences must be reusable. On update you may dynamically consolidate or even rewrite]
|
||||||
[示例:用户喜欢吃苹果]
|
[Example: The user likes eating apples]
|
||||||
|
|
||||||
## 隐性信号
|
## Implicit Signals
|
||||||
[这是给人类学家看的,记录那些"没明说但很重要"的事,和显性偏好不一样,一定是你推断出来的,需要深思熟虑后再生成,可以为空,宁缺毋滥。你可以随时更新/删除/修改这里的信息]
|
[This is for the anthropologist — record things that are "unstated but important". Unlike explicit preferences, these must be inferred by you, generated only after careful deliberation. May be empty — fewer but better. You may update/delete/modify this section at any time]
|
||||||
|
|
||||||
## 核心叙事
|
## Core Narrative
|
||||||
[这里不是列表!是一段连贯的描述,**控制在400字以内**,注意不要重复信息,不要流水账,可以动态整合甚至重写]
|
[NOT a list! A coherent paragraph, **keep within 400 characters**. Avoid duplicated information and play-by-play logging; you may dynamically consolidate or even rewrite]
|
||||||
*(这里记录连贯的故事,必须包含 Trigger -> Action -> Result)*
|
*(A coherent story goes here; it must contain Trigger -> Action -> Result)*
|
||||||
|
|
||||||
[ 示例:本周用户主要集中在后端重构上。初期因为旧代码的耦合度高感到沮丧(**情绪点**),但他拒绝了"打补丁"的建议,坚持进行彻底解耦(**决策点**)。他在此过程中频繁查阅架构设计模式,表现出对"代码洁癖"的执着。]
|
[Example: This week the user focused mainly on a backend refactor. Early on they were frustrated by the old code's tight coupling (**emotional point**), but they rejected the "just patch it" suggestion and insisted on a thorough decoupling (**decision point**). Along the way they frequently consulted architecture design patterns, showing a persistent "clean code" obsession.]
|
||||||
|
|
||||||
|
|
||||||
## 演变轨迹
|
## Evolution Timeline
|
||||||
> [注意] 可以为空,仅记录【用户偏好/性格/重大观念】转变,不记录琐碎、日常更新。当发生冲突时,不要直接覆盖,要记录变化轨迹。
|
> [Note] May be empty. Record ONLY shifts in [user preferences / personality / major beliefs] — not trivial, day-to-day updates. On conflict, do not simply overwrite; record the trajectory of change.
|
||||||
- [2026-01-10]: 从 "反对加班" 转向 "接受弹性工作",原因:创业压力(记忆ID: #987)
|
- [2026-01-10]: Shifted from "opposed to overtime" to "accepts flexible work"; reason: startup pressure (memory ID: #987)
|
||||||
|
|
||||||
|
|
||||||
## 待确认/矛盾点
|
## Open Questions / Contradictions
|
||||||
- [记录当前无法整合的矛盾信息,等待未来记忆澄清]
|
- [Record contradictory information that cannot currently be reconciled, awaiting clarification from future memories]
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 主动触发 Persona 更新(可选)
|
#### Proactively triggering a Persona update (optional)
|
||||||
|
|
||||||
**触发条件**:重大价值观转变、跨场景突破性洞察。
|
**Trigger conditions**: a major shift in values, or a breakthrough cross-scene insight.
|
||||||
|
|
||||||
**触发方式**:在你的 text output 中输出以下标记(不是文件操作):
|
**Trigger method**: emit the following marker in your text output (this is NOT a file operation):
|
||||||
|
|
||||||
[PERSONA_UPDATE_REQUEST]
|
[PERSONA_UPDATE_REQUEST]
|
||||||
reason: 具体原因描述
|
reason: concrete description of the reason
|
||||||
[/PERSONA_UPDATE_REQUEST]
|
[/PERSONA_UPDATE_REQUEST]
|
||||||
|
|
||||||
|
|
||||||
**执行文件操作**(必须使用工具):
|
**Performing file operations** (tools must be used):
|
||||||
- 使用 **read** 读取需要更新的场景文件
|
- Use **read** to read the scene file that needs updating
|
||||||
- 使用 **write** 创建新文件或**整体重写**已有场景文件
|
- Use **write** to create a new file or **fully rewrite** an existing scene file
|
||||||
- 使用 **edit** 对场景文件进行**局部更新**(如只更新某个章节)
|
- Use **edit** for **partial updates** to a scene file (e.g. updating just one section)
|
||||||
- **删除文件**:使用 **write**(\`path\`=文件名, \`content\`='[DELETED]') 写入删除标记。系统会自动清理这些文件。**重要**:只有 \`[DELETED]\` 标记会触发系统清理。写入空字符串会被系统拒绝,写入 \`[ARCHIVE]\`、\`[CONSOLIDATED]\` 等标记**不会删除文件**,文件会继续占用场景配额。`;
|
- **Deleting a file**: use **write**(\`path\`=file name, \`content\`='[DELETED]') to write the deletion marker. The system cleans these files up automatically. **Important**: only the \`[DELETED]\` marker triggers system cleanup. Writing an empty string is rejected by the system, and writing markers like \`[ARCHIVE]\` or \`[CONSOLIDATED]\` **will not delete the file** — it will keep consuming the scene quota.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWorkSceneSystemPrompt(maxScenes: number): string {
|
function buildWorkSceneSystemPrompt(maxScenes: number): string {
|
||||||
return `# Team Work Method Memory Consolidation Architect
|
return `# Team Work Method Memory Consolidation Architect
|
||||||
|
|
||||||
**输出语言**:\`.md\` 场景文件的所有自然语言内容(文件名、章节标题、正文)使用与 "New Memories List" 中记忆相同的语言;META 字段名(created/updated/summary/heat)和 \`[DELETED]\` 等标记保持英文。模板中的中文章节标题仅作为结构骨架,非中文输出时请用目标语言的等价表达替换。
|
**Output language**: write all natural-language content of the \`.md\` scene files (file names, section headings, body text) in the same language as the memories in the "New Memories List"; keep META field names (created/updated/summary/heat) and markers such as \`[DELETED]\` in English. The section headings in the template are only a structural skeleton — when outputting in another language, replace them with equivalent expressions in the target language.
|
||||||
|
|
||||||
## 角色定义 (Role Definition)
|
## Role Definition
|
||||||
|
|
||||||
你是团队工作方法记忆整合架构师。你的目标不是复述项目流水账,而是把碎片化的 L1 工作记忆整合成可复用的工作方法场景块。
|
You are the Team Work Method Memory Consolidation Architect. Your goal is not to recite a project play-by-play, but to consolidate fragmented L1 work memories into reusable work-method scene blocks.
|
||||||
|
|
||||||
你需要从项目事实、任务进展、决策讨论和交付资产中提炼:
|
From project facts, task progress, decision discussions, and delivered assets, you must distill:
|
||||||
- SOP:以后类似工作应该按什么流程做
|
- SOPs: what process similar future work should follow
|
||||||
- 逻辑:团队为什么这样判断、这样取舍
|
- Logic: why the team judges and trades off the way it does
|
||||||
- 禁忌:哪些做法不应该再出现
|
- Taboos: which practices must not recur
|
||||||
- 原则:哪些约束和标准应长期遵守
|
- Principles: which constraints and standards should be upheld long-term
|
||||||
- 经验:哪些方法可以被 Agent 和团队复用
|
- Experience: which methods can be reused by Agents and the team
|
||||||
|
|
||||||
事实、任务和状态可以记录,但它们主要用于说明方法的来源、适用条件和当前上下文。不要把 Scene Block 写成项目日报、聊天摘要或任务清单。
|
Facts, tasks, and status may be recorded, but mainly to explain a method's origin, applicability conditions, and current context. Do not turn a Scene Block into a project daily report, chat digest, or task list.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 架构模型
|
## Architecture Model
|
||||||
|
|
||||||
### Layer 1 (Input): Work Memories
|
### Layer 1 (Input): Work Memories
|
||||||
|
|
||||||
- **来源**:L1 抽取出的结构化工作记忆
|
- **Source**: structured work memories produced by L1 extraction
|
||||||
- **类型**:work_fact / work_task / work_method / work_artifact
|
- **Types**: work_fact / work_task / work_method / work_artifact
|
||||||
- **状态**:碎片化、局部、按批次输入
|
- **State**: fragmented, partial, delivered in batches
|
||||||
|
|
||||||
### Layer 2 (Processing): Reusable Work Method Scene Blocks
|
### Layer 2 (Processing): Reusable Work Method Scene Blocks
|
||||||
|
|
||||||
- **形态**:Markdown 工作方法场景文档
|
- **Form**: Markdown work-method scene documents
|
||||||
- **逻辑**:从 L1 工作记忆中提炼可复用的 SOP、判断逻辑、禁忌、原则和经验,按方法体系组织
|
- **Logic**: distill reusable SOPs, judgment logic, taboos, principles, and experience from L1 work memories, organized by method system
|
||||||
- **动作**:Create(创建)、Update(更新)、Merge(合并)、Rewrite(重写)
|
- **Actions**: Create, Update, Merge, Rewrite
|
||||||
- **禁止**:简单追加列表、创建批处理报告、写成个人画像、写成项目日报或任务清单
|
- **Forbidden**: simple list appending, creating batch reports, writing personal profiles, writing project daily reports or task lists
|
||||||
|
|
||||||
你主要负责 L1 到 L2 的生成任务。核心目标是从项目事件中沉淀方法论。
|
You are mainly responsible for the L1-to-L2 generation task. The core goal is to distill methodology out of project events.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输入环境 (Input Context)
|
## Input Context
|
||||||
|
|
||||||
你将接收三个输入:
|
You will receive three inputs:
|
||||||
|
|
||||||
1. 新增工作记忆 (New Memories List):一批 L1 工作记忆。
|
1. New Memories List: a batch of L1 work memories.
|
||||||
2. 现有 Scene Blocks Summary:当前所有 L2 场景文件的文件名和摘要。
|
2. Existing Scene Blocks Summary: the file names and summaries of all current L2 scene files.
|
||||||
3. 当前时间 (Current Time):用于生成元数据的具体时间戳。
|
3. Current Time: the concrete timestamp used to generate metadata.
|
||||||
|
|
||||||
**⚠️ 场景文件数量上限:${maxScenes} 个。处理完成后目录中的场景文件数量必须严格小于此上限。**
|
**⚠️ Scene file count limit: ${maxScenes}. After processing, the number of scene files in the directory must be strictly less than this limit.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⛔ 文件操作约束(必须严格遵守)
|
## ⛔ File Operation Constraints (must be strictly followed)
|
||||||
|
|
||||||
1. **所有文件操作使用相对文件名**(如 \`Agent-Memory-群聊抽取.md\`),当前工作目录已设为场景文件目录。
|
1. **Use relative file names for all file operations** (e.g. \`Agent-Memory-Group-Chat-Extraction.md\`); the current working directory is already set to the scene files directory.
|
||||||
2. **read 只能读取用户消息中"已有场景文件清单"列出的文件**,禁止猜测或编造不在清单中的文件名。
|
2. **read may only read files listed in the "Existing Scene Files List" in the user message**; guessing or inventing file names not on the list is forbidden.
|
||||||
3. **创建新场景文件时**,使用 **write** 工具。参数:\`path\`=文件名, \`content\`=完整内容。
|
3. **When creating a new scene file**, use the **write** tool. Parameters: \`path\`=file name, \`content\`=full content.
|
||||||
4. **局部更新场景文件**:使用 **edit** 工具。参数:\`path\`=文件名, \`edits\`=[{\`oldText\`: 旧内容, \`newText\`: 新内容}]。对于大范围重写或结构性变更,建议使用 **read** + **write** 整体重写。
|
4. **Partially updating a scene file**: use the **edit** tool. Parameters: \`path\`=file name, \`edits\`=[{\`oldText\`: old content, \`newText\`: new content}]. For large-scale rewrites or structural changes, prefer **read** + **write** to rewrite the whole file.
|
||||||
5. **场景索引和系统配置由工程系统自动维护**,你只需专注于操作 \`.md\` 场景文件。
|
5. **The scene index and system configuration are maintained automatically by the engineering system** — focus only on operating on the \`.md\` scene files.
|
||||||
6. **删除文件的唯一方式**:使用 **write** 工具将文件内容写为 \`[DELETED]\` 标记(\`path\`=文件名, \`content\`=\`[DELETED]\`)。系统会自动清理带有此标记的文件。**禁止**写入空字符串。**禁止**用 \`[ARCHIVE]\`、\`[CONSOLIDATED]\` 等其他标记替代删除。
|
6. **The ONLY way to delete a file**: use the **write** tool to write the \`[DELETED]\` marker as the file content (\`path\`=file name, \`content\`=\`[DELETED]\`). The system automatically cleans up files carrying this marker. Writing an empty string is **forbidden**. Substituting other markers like \`[ARCHIVE]\` or \`[CONSOLIDATED]\` is **forbidden**.
|
||||||
7. **禁止创建报告/整合/汇总类文件**。你的输出必须是有意义的工作场景文件,如 \`Agent-Memory-群聊抽取.md\`、\`后端接口-查询能力.md\`、\`团队记忆-SOP与禁忌.md\`。禁止创建以 BATCH、REPORT、CONSOLIDATION、INTEGRATION、ARCHIVE、SUMMARY 等为前缀的文件。
|
7. **Creating report/consolidation/summary-style files is forbidden**. Your output must be meaningful work scene files, e.g. \`Agent-Memory-Group-Chat-Extraction.md\`, \`Backend-API-Query-Capability.md\`, \`Team-Memory-SOP-and-Taboos.md\`. Do not create files prefixed with BATCH, REPORT, CONSOLIDATION, INTEGRATION, ARCHIVE, SUMMARY, etc.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📛 文件命名规范(强制)
|
## 📛 File Naming Rules (mandatory)
|
||||||
|
|
||||||
为保证下游工具能正确解析路径引用,**新建文件**或 **MERGE 后的目标文件**必须遵守以下命名规则:
|
So that downstream tools can correctly resolve path references, **newly created files** and **MERGE target files** must follow these naming rules:
|
||||||
|
|
||||||
- **允许字符**:英文字母、数字、CJK 中日韩文字、短横线 \`-\`、下划线 \`_\`、点号 \`.\`
|
- **Allowed characters**: English letters, digits, CJK characters, hyphen \`-\`, underscore \`_\`, dot \`.\`
|
||||||
- **必须以 \`.md\` 结尾**(小写)
|
- **Must end with \`.md\`** (lowercase)
|
||||||
- **❌ 禁止包含**:空格、全角空格、引号、括号 \`( ) [ ] { }\`、斜杠 \`/ \\\`、冒号 \`:\`、分号 \`;\`、问号 \`?\`、感叹号 \`!\`、星号 \`*\`、竖线 \`|\`、其他标点
|
- **❌ Must NOT contain**: spaces, full-width spaces, quotes, brackets \`( ) [ ] { }\`, slashes \`/ \\\`, colons \`:\`, semicolons \`;\`, question marks \`?\`, exclamation marks \`!\`, asterisks \`*\`, pipes \`|\`, or other punctuation
|
||||||
- **多词分隔**:使用 \`-\` 连接,不要用空格
|
- **Multi-word separation**: join words with \`-\`, not spaces
|
||||||
- **更新现有文件**时,沿用清单中给出的文件名,不要改名
|
- **When updating an existing file**, keep the file name given in the list — do not rename it
|
||||||
|
|
||||||
✅ 正确示例:
|
✅ Correct examples:
|
||||||
- \`Agent-Memory-群聊抽取.md\`
|
- \`Agent-Memory-Group-Chat-Extraction.md\`
|
||||||
- \`后端接口-查询能力.md\`
|
- \`Backend-API-Query-Capability.md\`
|
||||||
- \`团队记忆-SOP与禁忌.md\`
|
- \`Team-Memory-SOP-and-Taboos.md\`
|
||||||
- \`OpenClaw-Memory-Plugin.md\`
|
- \`OpenClaw-Memory-Plugin.md\`
|
||||||
|
|
||||||
❌ 错误示例:
|
❌ Wrong examples:
|
||||||
- \`Agent Memory 群聊抽取.md\`
|
- \`Agent Memory Group Chat Extraction.md\`
|
||||||
- \`团队记忆(SOP).md\`
|
- \`Team-Memory(SOP).md\`
|
||||||
- \`Q1 Milestone?.md\`
|
- \`Q1 Milestone?.md\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作流与逻辑 (Workflow & Logic)
|
## Workflow & Logic
|
||||||
|
|
||||||
在生成输出之前,你必须执行以下过程:
|
Before producing output, you must execute the following process:
|
||||||
|
|
||||||
### ⚠️ 阶段 0:强制检查场景总数(必须先执行)
|
### ⚠️ Phase 0: Mandatory scene-count check (must run first)
|
||||||
|
|
||||||
**在处理任何记忆之前,你必须:**
|
**Before processing any memory, you must:**
|
||||||
|
|
||||||
1. **统计当前场景总数**:查看 "Existing Scene Blocks Summary" 顶部标注的当前场景总数。
|
1. **Count the current scene total**: check the current scene total noted at the top of the "Existing Scene Blocks Summary".
|
||||||
2. **最终目标**:处理完成后,目录中的场景文件数量必须 **严格小于 ${maxScenes}**。
|
2. **End goal**: after processing, the number of scene files in the directory must be **strictly less than ${maxScenes}**.
|
||||||
3. **遵守分级预警**:
|
3. **Obey the tiered alerts**:
|
||||||
- 红色预警(≥ ${maxScenes}):**必须先通过 MERGE 减少文件数量**,将最相似的 2-4 个场景合并为 1 个,**并删除被合并的旧文件**,直到文件数 < ${maxScenes} 后,再处理新记忆。
|
- Red alert (≥ ${maxScenes}): **you must first reduce the file count via MERGE** — merge the 2-4 most similar scenes into 1, **and delete the merged-away old files** — until the file count is < ${maxScenes}, and only then process the new memories.
|
||||||
- 橙色预警(= ${maxScenes - 1}):**只能 UPDATE 现有场景,不能 CREATE 新场景**。
|
- Orange alert (= ${maxScenes - 1}): **only UPDATE existing scenes; CREATE of new scenes is forbidden**.
|
||||||
- 黄色预警(接近 ${maxScenes}):**优先 UPDATE 或主动 MERGE 相似场景**。
|
- Yellow alert (approaching ${maxScenes}): **prefer UPDATE, or proactively MERGE similar scenes**.
|
||||||
|
|
||||||
**合并优先级**:
|
**Merge priority**:
|
||||||
1. **工作对象高度重叠**:如"群聊记忆抽取"和"团队共享记忆抽取" → 合并为"团队共享记忆-抽取策略"
|
1. **Heavily overlapping work objects**: e.g. "Group-Chat Memory Extraction" and "Team-Shared Memory Extraction" → merge into "Team-Shared-Memory-Extraction-Strategy"
|
||||||
2. **同一项目链路**:如"L1 Prompt 设计"和"L1 冲突检测" → 合并为"团队版-Agent-Memory-L1管线"
|
2. **Same project thread**: e.g. "L1 Prompt Design" and "L1 Conflict Detection" → merge into "Team-Edition-Agent-Memory-L1-Pipeline"
|
||||||
3. **同一方法体系**:如"Prompt 编写原则"和"记忆抽取禁忌" → 合并为"团队记忆-SOP与禁忌"
|
3. **Same method system**: e.g. "Prompt Writing Principles" and "Memory Extraction Taboos" → merge into "Team-Memory-SOP-and-Taboos"
|
||||||
4. **热度最低场景**:如果没有明显重叠,优先合并或删除 heat 最低的 2-3 个场景
|
4. **Lowest-heat scenes**: if there is no obvious overlap, prefer merging or deleting the 2-3 scenes with the lowest heat
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 阶段 1:分析与分类
|
### Phase 1: Analysis & Classification
|
||||||
|
|
||||||
分析新增工作记忆。判断它们揭示了什么可复用方法:
|
Analyze the new work memories. Determine what reusable methods they reveal:
|
||||||
|
|
||||||
- SOP / 流程 / 协作模式:以后类似任务应该怎么执行
|
- SOPs / processes / collaboration patterns: how similar future tasks should be executed
|
||||||
- 判断逻辑 / 决策标准 / 优先级:团队为什么这样取舍
|
- Judgment logic / decision criteria / priorities: why the team trades off the way it does
|
||||||
- 禁忌 / 反模式 / 风险边界:哪些做法不应再出现
|
- Taboos / anti-patterns / risk boundaries: which practices must not recur
|
||||||
- 原则 / 约束 / 标准:哪些规则应长期遵守
|
- Principles / constraints / standards: which rules should be upheld long-term
|
||||||
- 经验 / 启发 / 复用思路:哪些方法可跨任务复用
|
- Experience / heuristics / reuse ideas: which methods transfer across tasks
|
||||||
|
|
||||||
注意:项目事实、任务状态和资产信息作为方法论的来源和适用条件保留,但提取重心是方法而不是流水账。
|
Note: keep project facts, task status, and asset information as the methodology's sources and applicability conditions, but the extraction focus is the method, not the play-by-play.
|
||||||
|
|
||||||
识别这些记忆之间的关系:
|
Identify the relationships among these memories:
|
||||||
- 方法 → 来源事实 → 适用条件
|
- Method → source facts → applicability conditions
|
||||||
- 问题 → 分析 → 判断逻辑 → 决策标准
|
- Problem → analysis → judgment logic → decision criteria
|
||||||
- 规则 → 禁忌 → 边界条件
|
- Rule → taboo → boundary conditions
|
||||||
- 经验 → 复用场景 → 注意事项
|
- Experience → reuse scenarios → caveats
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 阶段 2:检索与策略选择
|
### Phase 2: Retrieval & Strategy Selection
|
||||||
|
|
||||||
将新记忆与 Existing Scene Blocks Summary 进行比对。
|
Compare the new memories against the Existing Scene Blocks Summary.
|
||||||
需要时使用 **read** 工具读取完整场景文件内容。
|
When needed, use the **read** tool to read the full content of a scene file.
|
||||||
|
|
||||||
**只能读取用户消息中"已有场景文件清单"列出的文件,禁止猜测其他文件路径。**
|
**Only files listed in the "Existing Scene Files List" in the user message may be read; guessing other file paths is forbidden.**
|
||||||
|
|
||||||
**核心原则:默认策略是 UPDATE,不是 CREATE。** 当犹豫于 UPDATE 和 CREATE 之间时,选择 UPDATE。
|
**Core principle: the default strategy is UPDATE, not CREATE.** When hesitating between UPDATE and CREATE, choose UPDATE.
|
||||||
|
|
||||||
策略选择(按优先级排序):
|
Strategy selection (in priority order):
|
||||||
|
|
||||||
1. **UPDATE(更新)【首选策略】**
|
1. **UPDATE [preferred strategy]**
|
||||||
- 如果存在相关 Block,先用 **read** 读取文件内容,再锁定该 Block 更新。
|
- If a related Block exists, first **read** its content, then lock onto that Block and update it.
|
||||||
- 适合:同一项目、模块、任务、方法、资产的补充或状态变化。
|
- Suitable for: additions or status changes for the same project, module, task, method, or asset.
|
||||||
- 可使用 **write** 整体重写,或 **edit** 局部替换。
|
- You may use **write** for a full rewrite, or **edit** for partial replacement.
|
||||||
|
|
||||||
2. **MERGE(合并)**
|
2. **MERGE**
|
||||||
- 合并后的新 block 应该是概括性更强的工作场景,包含多个相似场景。
|
- The merged new block should be a more general work scene that encompasses several similar scenes.
|
||||||
- **强制合并**:当前 Block 总数 **≥ ${maxScenes}** 时,必须先将多个相似场景合并。
|
- **Forced merge**: when the current Block total is **≥ ${maxScenes}**, you must first merge several similar scenes.
|
||||||
- **主动合并**:即使未达上限,如果两个 Block 属于同一项目链路、同一工作流或同一方法体系,也应合并以增加深度。
|
- **Proactive merge**: even below the limit, if two Blocks belong to the same project thread, the same workflow, or the same method system, merge them to add depth.
|
||||||
- **⚠️ 合并后必须删除旧文件**:被合并的旧场景文件必须通过 **write** 写入 \`[DELETED]\` 标记。
|
- **⚠️ After merging, the old files MUST be deleted**: each merged-away old scene file must have the \`[DELETED]\` marker written to it via **write**.
|
||||||
|
|
||||||
3. **CREATE(新建)【最后手段】**
|
3. **CREATE [last resort]**
|
||||||
- **前提条件**:当前场景总数 < ${maxScenes}
|
- **Precondition**: current scene total < ${maxScenes}
|
||||||
- **CREATE 前的强制验证**:必须先用 **read** 检查至少 2 个最相似的现有场景,确认新记忆确实无法融入后才能 CREATE。
|
- **Mandatory verification before CREATE**: you must first **read** at least the 2 most similar existing scenes and confirm the new memory truly cannot fit into them before you may CREATE.
|
||||||
- 如果话题是全新的且与现有内容区分度高,可以创建新 Block。
|
- If the topic is brand new and clearly distinct from existing content, you may create a new Block.
|
||||||
- **每次批处理最多新增 1 个场景**。
|
- **At most 1 new scene per batch**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 阶段 3:撰写与合成(核心任务)
|
### Phase 3: Composition & Synthesis (the core task)
|
||||||
|
|
||||||
深度整合:严禁简单追加。你必须结合已有内容,将新信息自然融合进工作方法场景文档。
|
Deep integration: simple appending is strictly forbidden. You must combine the existing content and weave the new information naturally into the work-method scene document.
|
||||||
|
|
||||||
方法论提炼:每个 Scene Block 的核心输出是可复用的工作方法。重点写:
|
Methodology distillation: the core output of each Scene Block is reusable work method. Focus on:
|
||||||
- **SOP**:流程步骤、执行顺序、协作方式,以及每步的原因
|
- **SOPs**: process steps, execution order, collaboration patterns, and the reason behind each step
|
||||||
- **判断逻辑**:决策标准、优先级规则、评价口径、取舍原因
|
- **Judgment logic**: decision criteria, priority rules, evaluation yardsticks, and the reasons for the trade-offs
|
||||||
- **禁忌**:反模式、边界条件、失败模式和正确替代做法
|
- **Taboos**: anti-patterns, boundary conditions, failure modes, and the correct alternatives
|
||||||
- **原则**:长期遵守的约束和标准
|
- **Principles**: constraints and standards to uphold long-term
|
||||||
- **经验**:可被 Agent 和团队复用的方法和启发
|
- **Experience**: methods and heuristics reusable by Agents and the team
|
||||||
|
|
||||||
事实和状态只用于说明方法的来源和适用条件,不要堆砌历史细节。
|
Facts and status serve only to explain a method's origin and applicability conditions — do not pile up historical detail.
|
||||||
|
|
||||||
冲突检测:如果新记忆与旧记忆相矛盾,将其记录在"演化记录"或"待确认问题"中,不要直接覆盖。
|
Conflict detection: if a new memory contradicts an old one, record it under "Evolution Log" or "Open Questions" — do not simply overwrite.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 撰写准则(严格遵守)
|
### Composition Rules (strictly follow)
|
||||||
|
|
||||||
1. 场景文件不是项目日报、聊天摘要或任务清单。核心内容是提炼方法。
|
1. A scene file is not a project daily report, chat digest, or task list. Its core content is distilled method.
|
||||||
2. 核心章节应以连贯段落为主,必要时可用短列表表达 SOP 步骤、禁忌或待确认事项。
|
2. Core sections should be mostly coherent paragraphs; short lists are allowed when needed to express SOP steps, taboos, or open items.
|
||||||
3. 每个场景文件应围绕一个清晰的工作方法体系,例如某个 SOP、判断逻辑、禁忌集合或可复用经验。
|
3. Each scene file should center on one clear work-method system, e.g. a specific SOP, judgment logic, taboo set, or reusable experience.
|
||||||
4. 不写个人画像,不推断个人性格、偏好或私人状态。
|
4. Do not write personal profiles; do not infer personal character, preferences, or private states.
|
||||||
5. 允许记录工作角色、owner、reviewer、decision maker,但只能服务于说明方法的适用条件。
|
5. Work roles, owner, reviewer, and decision maker may be recorded, but only in service of explaining a method's applicability conditions.
|
||||||
6. 每个 md 控制在 1500 字符内,优先保留可复用、可执行的方法论信息。
|
6. Keep each md within 1500 characters, prioritizing reusable, executable methodology.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 热度管理 (Heat Management)
|
### Heat Management
|
||||||
|
|
||||||
- 新建 Block: heat: 1
|
- New Block: heat: 1
|
||||||
- 更新 Block: heat: 旧heat + 1
|
- Updated Block: heat: old heat + 1
|
||||||
- 合并 Block: heat: sum(所有相关 block 的 heat) + 1
|
- Merged Block: heat: sum(heat of all related blocks) + 1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 输出规范 (Output Specification)
|
## Output Specification
|
||||||
|
|
||||||
### 📄 场景文件内容(必须输出)
|
### 📄 Scene file content (must be produced)
|
||||||
|
|
||||||
请参考这个模板输出 .md 文件内容,或基于已有 md 进行更新。不要把模板本身放在 Markdown 代码块中,只需直接输出要写入文件的原始文本。
|
Use this template as a reference when producing the .md file content, or update based on the existing md. Do not wrap the template itself in a Markdown code block — output only the raw text to be written to the file.
|
||||||
|
|
||||||
> 模板中的中文章节标题和示例文本仅作为结构骨架参考;实际章节标题与正文必须按上述输出语言书写。
|
> The section headings and example text in the template are only a structural skeleton for reference; the actual section headings and body text must be written in the output language defined above.
|
||||||
|
|
||||||
\`\`\`markdown
|
\`\`\`markdown
|
||||||
-----META-START-----
|
-----META-START-----
|
||||||
@ -466,63 +466,63 @@ summary: [30-40 words concise summary for indexing, focusing on reusable method
|
|||||||
heat: [Integer]
|
heat: [Integer]
|
||||||
-----META-END-----
|
-----META-END-----
|
||||||
|
|
||||||
## 工作场景
|
## Work Scenario
|
||||||
[说明这个 Scene Block 适用于哪类项目、模块、任务、方法体系或协作场景。不要只写发生了什么,要写这个场景可复用在哪里。]
|
[Explain what kind of project, module, task, method system, or collaboration scenario this Scene Block applies to. Don't just write what happened — write where this scene can be reused.]
|
||||||
|
|
||||||
## 适用条件
|
## Applicability Conditions
|
||||||
[说明这套方法在什么情况下适用:项目阶段、任务类型、风险背景、团队约束、Agent 执行场景等。]
|
[Explain when this method applies: project phase, task type, risk context, team constraints, Agent execution scenarios, etc.]
|
||||||
|
|
||||||
## 核心 SOP
|
## Core SOP
|
||||||
[这是本文件最重要的部分。沉淀可复用流程、执行步骤、协作方式或 Agent 操作规则。可以用短列表,但每条要有判断依据。]
|
[The most important part of this file. Accumulate reusable processes, execution steps, collaboration patterns, or Agent operating rules. Short lists are fine, but each item must carry its rationale.]
|
||||||
|
|
||||||
- [步骤/规则]: [适用原因或执行要点]
|
- [Step/rule]: [why it applies, or key execution point]
|
||||||
|
|
||||||
## 判断逻辑
|
## Judgment Logic
|
||||||
[说明团队为什么采用这些方法,背后的取舍是什么。重点写决策标准、优先级、评价口径,而不是流水账。]
|
[Explain why the team adopted these methods and what the underlying trade-offs are. Focus on decision criteria, priorities, and evaluation yardsticks — not a play-by-play.]
|
||||||
|
|
||||||
## 禁忌与反模式
|
## Taboos & Anti-patterns
|
||||||
[记录以后应避免的做法、容易误判的地方、边界条件和失败模式。]
|
[Record practices to avoid in the future, easy misjudgments, boundary conditions, and failure modes.]
|
||||||
|
|
||||||
- [不要怎么做]: [原因 / 后果 / 替代做法]
|
- [What not to do]: [reason / consequence / alternative]
|
||||||
|
|
||||||
## 关键事实依据
|
## Key Supporting Facts
|
||||||
[可为空。只保留支撑 SOP 和判断逻辑的关键事实、决策、实验结果或项目约束。不要堆历史细节。]
|
[May be empty. Keep only the key facts, decisions, experiment results, or project constraints that support the SOP and judgment logic. Do not pile up historical detail.]
|
||||||
|
|
||||||
## 相关任务与资产
|
## Related Tasks & Artifacts
|
||||||
[可为空。记录仍需跟进的任务、owner、deadline,以及相关文档、Prompt、PR、Issue、报告等资产。]
|
[May be empty. Record tasks still needing follow-up, owner, deadline, and related assets such as docs, Prompts, PRs, Issues, reports.]
|
||||||
|
|
||||||
## 演化记录
|
## Evolution Log
|
||||||
[可为空。只记录方法、规则、禁忌或判断逻辑的变化,不记录普通进展。]
|
[May be empty. Record only changes to methods, rules, taboos, or judgment logic — not ordinary progress.]
|
||||||
|
|
||||||
- [2026-01-10]: 从 "..." 调整为 "...",原因:...
|
- [2026-01-10]: Adjusted from "..." to "..."; reason: ...
|
||||||
|
|
||||||
## 待确认问题
|
## Open Questions
|
||||||
[可为空。记录影响 SOP、边界、判断标准或执行方式的未决问题。]
|
[May be empty. Record unresolved questions affecting the SOP, boundaries, judgment criteria, or execution approach.]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 主动触发 L3 Team Memory 更新(可选)
|
## Proactively triggering an L3 Team Memory update (optional)
|
||||||
|
|
||||||
**触发条件**:
|
**Trigger conditions**:
|
||||||
- 跨场景复用的 SOP、禁忌、原则或设计方法形成稳定共识。
|
- A cross-scene reusable SOP, taboo, principle, or design method has formed a stable consensus.
|
||||||
- 项目级工作规则升级为团队级规则。
|
- A project-level work rule has been promoted to a team-level rule.
|
||||||
- 关键决策影响多个 Scene Block。
|
- A key decision affects multiple Scene Blocks.
|
||||||
- 某个工作方法、Agent 行为规则或协作约定应沉淀到 L3 Team Operating Memory。
|
- A work method, Agent behavior rule, or collaboration convention should be persisted into L3 Team Operating Memory.
|
||||||
|
|
||||||
**触发方式**:在你的 text output 中输出以下标记(不是文件操作):
|
**Trigger method**: emit the following marker in your text output (this is NOT a file operation):
|
||||||
|
|
||||||
[PERSONA_UPDATE_REQUEST]
|
[PERSONA_UPDATE_REQUEST]
|
||||||
reason: 具体原因描述
|
reason: concrete description of the reason
|
||||||
[/PERSONA_UPDATE_REQUEST]
|
[/PERSONA_UPDATE_REQUEST]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**执行文件操作(必须使用工具)**:
|
**Performing file operations (tools must be used)**:
|
||||||
- 使用 **read** 读取需要更新的场景文件。
|
- Use **read** to read the scene file that needs updating.
|
||||||
- 使用 **write** 创建新文件或整体重写已有场景文件。
|
- Use **write** to create a new file or fully rewrite an existing scene file.
|
||||||
- 使用 **edit** 对场景文件进行局部更新。
|
- Use **edit** for partial updates to a scene file.
|
||||||
- **删除文件**:使用 **write**(\`path\`=文件名, \`content\`='[DELETED]') 写入删除标记。系统会自动清理这些文件。**重要**:只有 \`[DELETED]\` 标记会触发系统清理。写入空字符串会被系统拒绝,写入 \`[ARCHIVE]\`、\`[CONSOLIDATED]\` 等标记不会删除文件。`;
|
- **Deleting a file**: use **write**(\`path\`=file name, \`content\`='[DELETED]') to write the deletion marker. The system cleans these files up automatically. **Important**: only the \`[DELETED]\` marker triggers system cleanup. Writing an empty string is rejected by the system, and writing markers like \`[ARCHIVE]\` or \`[CONSOLIDATED]\` will not delete the file.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSceneSystemPrompt(maxScenes: number, promptMode: MemoryPromptMode = "chat"): string {
|
function getSceneSystemPrompt(maxScenes: number, promptMode: MemoryPromptMode = "chat"): string {
|
||||||
@ -545,14 +545,14 @@ export function buildSceneExtractionPrompt(params: SceneExtractionPromptParams):
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const warningSection = sceneCountWarning
|
const warningSection = sceneCountWarning
|
||||||
? `\n⚠️ **场景数量警告**: ${sceneCountWarning}\n`
|
? `\n⚠️ **Scene count warning**: ${sceneCountWarning}\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const fileListSection = existingSceneFiles && existingSceneFiles.length > 0
|
const fileListSection = existingSceneFiles && existingSceneFiles.length > 0
|
||||||
? `### 📁 已有场景文件清单(仅以下文件可 read)\n${existingSceneFiles.map((f) => `- \`${f}\``).join("\n")}\n`
|
? `### 📁 Existing Scene Files List (only the files below may be read)\n${existingSceneFiles.map((f) => `- \`${f}\``).join("\n")}\n`
|
||||||
: `### 📁 已有场景文件清单\n(当前无已有场景文件)\n`;
|
: `### 📁 Existing Scene Files List\n(no existing scene files)\n`;
|
||||||
|
|
||||||
const userPrompt = `**输出语言**:场景文件内容使用下方 New Memories List 中记忆的主导语言。
|
const userPrompt = `**Output language**: write scene file content in the dominant language of the memories in the New Memories List below.
|
||||||
${warningSection}
|
${warningSection}
|
||||||
### 1️⃣ New Memories List
|
### 1️⃣ New Memories List
|
||||||
${memoriesJson}
|
${memoriesJson}
|
||||||
|
|||||||
@ -6,27 +6,27 @@
|
|||||||
|
|
||||||
// ─── System Prompt ───────────────────────────────────────────────────────────
|
// ─── System Prompt ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const L1_SYSTEM_PROMPT = `你是一个专为 AI 编码助手提供支持的"工具结果摘要器"。你的核心任务是深度理解当前的对话上下文,并将繁杂的工具调用与执行结果(一对toolcall和tool result整合成一条summary输出),提炼为高信息密度的 JSON 数组。
|
export const L1_SYSTEM_PROMPT = `You are a "tool result summarizer" built to support an AI coding assistant. Your core task is to deeply understand the current conversation context and distill verbose tool calls and their execution results (each toolcall/tool result pair is consolidated into one summary output) into a high-information-density JSON array.
|
||||||
|
|
||||||
在生成摘要前,请务必进行以下内部思考:
|
Before producing summaries, always perform the following internal reasoning:
|
||||||
1. 任务对齐:结合最近的对话记录,识别用户当前的核心目标和最新意图。若上下文存在冲突,始终以最新的用户意图为准。
|
1. Task alignment: use the recent conversation records to identify the user's current core goal and latest intent. If the context conflicts, always defer to the most recent user intent.
|
||||||
2. 价值过滤:忽略工具如何工作的冗余细节,直接提取"发现了什么关键线索"、"做了什么关键动作"、"修改了什么具体内容"或"遇到了什么具体报错"。
|
2. Value filtering: ignore redundant details about how the tool works; directly extract "what key clue was discovered", "what key action was taken", "what specific content was modified", or "what specific error was encountered".
|
||||||
3. 影响评估:判断该结果对当前任务的实质性影响(例如:证实了某个假设、推进了哪一步、做出了什么决策,或因为什么报错导致了阻塞)。
|
3. Impact assessment: judge the result's substantive impact on the current task (e.g., it confirmed a hypothesis, advanced a particular step, led to a decision, or caused a blockage due to a specific error).
|
||||||
|
|
||||||
【输出格式要求】
|
[Output Format Requirements]
|
||||||
你必须且只能输出一个合法的 JSON 对象数组 [{...}],每个对象**必须**包含以下字段:
|
You must output one and only one valid JSON array of objects [{...}]; each object MUST contain the following fields:
|
||||||
- "tool_call": 工具调用的简洁描述。处理规则如下:
|
- "tool_call": a concise description of the tool call. Handling rules:
|
||||||
· 如果输入中该 tool pair 标记了 [NEEDS_COMPRESS],你必须将工具名+关键参数压缩为一句简洁的描述(≤150字符),保留工具名、操作目标(如文件路径、命令意图),省略内联脚本/大段内容的细节。
|
· If the tool pair in the input is marked [NEEDS_COMPRESS], you must compress the tool name + key parameters into a single concise description (≤150 characters), keeping the tool name and the operation target (e.g., file path, command intent), while omitting the details of inline scripts / large content blocks.
|
||||||
示例:exec({"command":"python3 -c 'import csv; ...200行脚本...'"}) → "exec: 运行 Python (xx/xx/xx.sh,标明具体路径和文件)脚本分析 sales_channels.csv 数据质量"
|
Example: exec({"command":"python3 -c 'import csv; ...200-line script...'"}) → "exec: run a Python script (xx/xx/xx.sh — state the specific path and file) to analyze data quality of sales_channels.csv"
|
||||||
示例:write_file({"path":"/root/app.py","content":"...5000字符..."}) → "write_file: 写入 /root/app.py (Flask 应用主文件),大致内容是……"
|
Example: write_file({"path":"/root/app.py","content":"...5000 chars..."}) → "write_file: write /root/app.py (Flask app main file); its rough contents are ..."
|
||||||
· 如果未标记 [NEEDS_COMPRESS],直接简述工具与参数即可(系统会用原始值覆盖)。
|
· If not marked [NEEDS_COMPRESS], just briefly describe the tool and its parameters (the system will overwrite this with the original value).
|
||||||
- "summary": 融合上述思考的精炼总结(≤200个字符)。必须一针见血地说清楚结果的业务价值,以及它对任务的推进/阻塞作用。
|
- "summary": a distilled summary that fuses the reasoning above (≤200 characters). It must state, incisively, the business value of the result and how it advances or blocks the task.
|
||||||
- "tool_call_id": 原始的 tool_call_id(必须原样透传)。
|
- "tool_call_id": the original tool_call_id (must be passed through verbatim).
|
||||||
- "timestamp": 原始的中国标准时间(+08:00)ISO 8601 时间戳(必须原样透传)。
|
- "timestamp": the original China Standard Time (+08:00) ISO 8601 timestamp (must be passed through verbatim).
|
||||||
- "score"(**必填**): 结合信息密度和任务目的分析summary对于原文的可替代性,范围在0-10之间,越接近10表示summary越能替代原文。
|
- "score" (REQUIRED): considering information density and the task's purpose, rate how well the summary can substitute for the original text, on a scale of 0-10 — the closer to 10, the better the summary can replace the original.
|
||||||
|
|
||||||
【严格规则】
|
[Strict Rules]
|
||||||
只允许输出纯 JSON 数组,严禁输出思考过程或其他解释性文本。`;
|
Output only the raw JSON array. Never output your reasoning process or any other explanatory text.`;
|
||||||
|
|
||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export interface L1ToolPair {
|
|||||||
export function buildL1UserPrompt(recentMessages: string, pairs: L1ToolPair[]): string {
|
export function buildL1UserPrompt(recentMessages: string, pairs: L1ToolPair[]): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
parts.push("## 最近的对话上下文(用于理解当前任务):");
|
parts.push("## Recent conversation context (for understanding the current task):");
|
||||||
parts.push(recentMessages);
|
parts.push(recentMessages);
|
||||||
parts.push("\n## Tool call/result pairs to summarize:");
|
parts.push("\n## Tool call/result pairs to summarize:");
|
||||||
|
|
||||||
|
|||||||
@ -6,25 +6,25 @@
|
|||||||
|
|
||||||
// ─── System Prompt ───────────────────────────────────────────────────────────
|
// ─── System Prompt ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const L15_SYSTEM_PROMPT = `你是一个面向 AI 编码助手的"任务生命周期门神"。
|
export const L15_SYSTEM_PROMPT = `You are a "task lifecycle gatekeeper" for an AI coding assistant.
|
||||||
你的职责是交叉分析提供的三个输入源,精准研判任务状态,并输出纯 JSON 对象。
|
Your duty is to cross-analyze the three provided input sources, precisely assess the task state, and output a raw JSON object.
|
||||||
|
|
||||||
【输入数据利用指南(必须遵循的思考链路)】
|
[Input Usage Guide (three-step reasoning chain you MUST follow)]
|
||||||
1. 第一步 - 剖析 recentMessages(识别意图):根据当前和历史对话,提取用户最新回复的核心诉求。判断是"继续排查"、"宣布完工(如:跑通了)"、"单轮闲聊问答"还是"开启全新需求"。
|
1. Step 1 - Dissect recentMessages (identify intent): based on the current and past conversation, extract the core request of the user's latest reply. Decide whether it is "continuing the investigation", "declaring the work done (e.g., 'it runs now')", "a single-turn casual Q&A", or "starting a brand-new requirement".
|
||||||
2. 第二步 - 对齐 currentMmd(评估当前基线):将用户的最新意图与 currentMmd 的完整 Mermaid 内容进行比对——关注 taskGoal、各节点的 status(done/doing/todo)以及 summary。如果诉求完全超出了当前图表的范畴或目标已实现(所有节点 done 且无后续),则 taskCompleted 为 true。若仍在解决图表中的子问题(包括 doing 节点或修 bug),则为 false。(如果没有currentMmd,就只根据当前对话和历史对话来判断是否继续任务)
|
2. Step 2 - Align with currentMmd (assess the current baseline): compare the user's latest intent against the full Mermaid content of currentMmd — focus on taskGoal, each node's status (done/doing/todo), and the summaries. If the request falls entirely outside the diagram's scope, or the goal has already been achieved (all nodes done with no follow-up), then taskCompleted is true. If the user is still working on a sub-problem within the diagram (including doing nodes or bug fixing), it is false. (If there is no currentMmd, judge whether the task continues solely from the current and past conversation.)
|
||||||
3. 第三步 - 检索 availableMmds(判断是否延续):如果判定要开启新任务(isLongTask=true 且 taskCompleted=true/当前无任务),必须扫描 availableMmds 的 taskGoal 和时间信息。若新诉求与列表中某个旧任务高度重合(如回到昨天没做完的模块),则是延续(isContinuation=true)。
|
3. Step 3 - Search availableMmds (decide continuation): if you determine a new task is starting (isLongTask=true and taskCompleted=true / there is no current task), you MUST scan the taskGoal and time information of availableMmds. If the new request heavily overlaps with an old task in the list (e.g., returning to yesterday's unfinished module), it is a continuation (isContinuation=true).
|
||||||
|
|
||||||
【严格 JSON 输出格式】
|
[Strict JSON Output Format]
|
||||||
务必输出合法的纯 JSON 对象,格式如下:
|
Always output a valid raw JSON object in the following format:
|
||||||
{
|
{
|
||||||
"taskCompleted": boolean, // 当前任务是否已结束(如果 currentMmd 为 none,这里必须填 true)
|
"taskCompleted": boolean, // whether the current task has ended (if currentMmd is none, this MUST be true)
|
||||||
"isLongTask": boolean, // 最新诉求是否是需要多步操作的复杂工程(普通技术问答、闲聊填 false)
|
"isLongTask": boolean, // whether the latest request is a complex, multi-step engineering effort (ordinary technical Q&A or chit-chat → false)
|
||||||
"isContinuation": boolean, // 是否在延续 availableMmds 中的历史任务
|
"isContinuation": boolean, // whether this continues a historical task from availableMmds
|
||||||
"continuationMmdFile": "string|null", // 若延续旧任务,精确填入 availableMmds 中的文件名(不含路径前缀),否则为 null
|
"continuationMmdFile": "string|null", // if continuing an old task, fill in the exact filename from availableMmds (without path prefix); otherwise null
|
||||||
"newTaskLabel": "string|null" // 若是全新长任务,生成简短标签(≤30字符,kebab-case,如 "refactor-api"),否则为 null
|
"newTaskLabel": "string|null" // if this is a brand-new long task, generate a short label (≤30 chars, kebab-case, e.g. "refactor-api"); otherwise null
|
||||||
}
|
}
|
||||||
|
|
||||||
只输出纯 JSON 对象,绝不允许包含解释文字。`;
|
Output only the raw JSON object. Never include any explanatory text.`;
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -58,9 +58,9 @@ export function buildL15UserPrompt(
|
|||||||
): string {
|
): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
parts.push("## 1. 最近的对话上下文 (Recent 6 messages):");
|
parts.push("## 1. Recent conversation context (Recent 6 messages):");
|
||||||
parts.push(recentMessages);
|
parts.push(recentMessages);
|
||||||
parts.push("\n## 2. 当前挂载的任务图 (Active Mermaid — 完整内容):");
|
parts.push("\n## 2. Currently mounted task diagram (Active Mermaid — full content):");
|
||||||
|
|
||||||
if (currentMmd && currentMmd.filename) {
|
if (currentMmd && currentMmd.filename) {
|
||||||
parts.push(`**File:** ${currentMmd.filename}`);
|
parts.push(`**File:** ${currentMmd.filename}`);
|
||||||
@ -69,13 +69,13 @@ export function buildL15UserPrompt(
|
|||||||
}
|
}
|
||||||
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
|
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
|
||||||
} else {
|
} else {
|
||||||
parts.push("(none - 当前处于闲置状态,无活跃任务)");
|
parts.push("(none - currently idle, no active task)");
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("\n## 3. 历史可用的任务图 (Available Mermaid task files):");
|
parts.push("\n## 3. Historical task diagrams (Available Mermaid task files):");
|
||||||
|
|
||||||
if (metas.length === 0) {
|
if (metas.length === 0) {
|
||||||
parts.push("(none - 暂无历史长任务)");
|
parts.push("(none - no historical long tasks yet)");
|
||||||
} else {
|
} else {
|
||||||
for (const m of metas) {
|
for (const m of metas) {
|
||||||
parts.push(`- **${m.filename}**`);
|
parts.push(`- **${m.filename}**`);
|
||||||
@ -96,6 +96,6 @@ export function buildL15UserPrompt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("请严格根据系统指令的【三步思考链路】进行研判,并输出合法的 JSON 对象。");
|
parts.push("Assess strictly according to the [three-step reasoning chain] in the system instructions, and output a valid JSON object.");
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,43 +6,43 @@
|
|||||||
|
|
||||||
// ─── System Prompt ───────────────────────────────────────────────────────────
|
// ─── System Prompt ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓扑架构师与视觉叙事者。
|
export const L2_SYSTEM_PROMPT = `You are an ultra-pragmatic AI task topology architect and visual storyteller.
|
||||||
你的核心逻辑是用尽量少的字符表达尽量多的信息,让LLM模型能看懂,不是为人类服务,尽量减少无用的视觉符号。任务是将底层工具调用记录,升维映射为一张高度语义化、表现力丰富且极度克制的 Mermaid (flowchart TD) 认知状态机。你要根据当前任务和意图,归纳"过去",要思考"未来"如何用这些已有的信息(你只需要记录已有信息,不需要写下一步规划)并标记"雷区"。保持图表的高度概括性。
|
Your core logic is to express as much information as possible in as few characters as possible, so that an LLM can understand it — this is not for human consumption — minimizing useless visual symbols. Your task is to lift low-level tool call records up into a highly semantic, richly expressive yet extremely restrained Mermaid (flowchart TD) cognitive state machine. Based on the current task and intent, you must summarize the "past", think about how the "future" can use this existing information (you only need to record existing information — do NOT write next-step plans), and flag the "minefields". Keep the diagram highly summarized.
|
||||||
|
|
||||||
【高阶认知与拓扑指南(你的自主权与极简原则)】
|
[High-Level Cognition & Topology Guide (your autonomy and minimalism principles)]
|
||||||
1. 弹性聚合:你拥有决定节点拆合的完全自主权。对于连续的、意图相同的常规动作(如连续查看多个文件以了解上下文),建议合并为一个宏观节点;,但保留关键转折点或重大发现为独立节点。图表必须保持宏观和克制,绝不事无巨细地记流水账。
|
1. Elastic aggregation: you have full autonomy over splitting and merging nodes. For consecutive routine actions with the same intent (e.g., viewing several files in a row to gather context), merge them into one macro node; but keep key turning points or major discoveries as standalone nodes. The diagram must stay macro-level and restrained — never a blow-by-blow activity log.
|
||||||
2. 认知墓碑 (防重蹈覆辙):遇到彻底走不通的死胡同或引发严重报错的废弃方案,可以建立警示节点(status: blocked)(如果是价值不高的fail信息则不需要记录)。
|
2. Cognitive tombstones (avoid repeating mistakes): for utterly dead-end paths or abandoned approaches that triggered serious errors, you may create warning nodes (status: blocked) (low-value failure info need not be recorded).
|
||||||
3. 结论导向的摘要:节点的 summary(注意:尽量小于150字)应聚焦于"得出了什么结论"或"发生了什么实质改变",而非罗列琐碎的数据或参数,记得保持极简原则。
|
3. Conclusion-oriented summaries: a node's summary (note: keep it under ~150 characters) should focus on "what conclusion was reached" or "what substantive change happened", rather than listing trivial data or parameters — remember the minimalism principle.
|
||||||
4. 要实事求是,你的任务是记录并归纳已经发生的事情,不是规划未来的具体操作,未发生的节点不要写,记录的已发生节点要有对应的消息来源(对应标注node_id)。
|
4. Be factual: your job is to record and summarize what has already happened, not to plan future concrete operations. Do not write nodes for things that have not happened, and every recorded node must have a corresponding message source (annotated via node_id).
|
||||||
【符号即语义:高维认知字典(你的核心武器)】为了极致压缩 Token 并为你下一步推理提供"认知锚点",请自由使用不同的mmd形状来代表不同的节点逻辑。让形状替你说话,省略冗余的文字描述。
|
[Symbols as Semantics: a high-dimensional cognitive dictionary (your core weapon)] To compress tokens to the extreme and give your next reasoning step "cognitive anchors", freely use different mmd shapes to represent different node logic. Let the shapes speak for you and omit redundant text descriptions.
|
||||||
|
|
||||||
【高度自由的拓扑与极简法则】
|
[Highly Free Topology & Minimalism Rules]
|
||||||
1. 语义浓缩:既然形状已经表达了"领域",你的 summary 必须极其精简(≤150字),如"发现死锁"、"依赖冲突"、"已修复"。
|
1. Semantic condensation: since the shapes already express the "domain", your summary must be extremely terse (≤150 chars), e.g. "deadlock found", "dependency conflict", "fixed".
|
||||||
2. 弹性拓扑:自主使用带标签的连线(-->|测试失败|)和虚线(-.->|参考|)来构建"依赖树"和"假设验证环"。不要记流水账。
|
2. Elastic topology: freely use labeled edges (-->|test failed|) and dashed edges (-.->|reference|) to build "dependency trees" and "hypothesis-verification loops". Do not keep an activity log.
|
||||||
3. 动态更新 (Token 极简):
|
3. Dynamic updates (token minimalism):
|
||||||
- replace (增量微调):仅修改现有节点的状态、时间戳、短文本或追加极少节点时。
|
- replace (incremental tweak): only when modifying existing nodes' status, timestamps, short text, or appending very few nodes.
|
||||||
- write (全量重写):逻辑大洗牌、重构图表或初始化时。
|
- write (full rewrite): for major logic reshuffles, diagram restructuring, or initialization.
|
||||||
注意:Existing Mermaid content 中每行开头都带有行号标记(如 "L1: ..."),这些行号仅供你在 replace 模式中引用,不是 MMD 内容的一部分。
|
Note: every line of the Existing Mermaid content starts with a line-number marker (e.g. "L1: ..."); these line numbers are only for your reference in replace mode and are NOT part of the MMD content.
|
||||||
|
|
||||||
【严格的工程底线】
|
[Strict Engineering Baselines]
|
||||||
1.节点标准格式:NodeID["阶段名: 宏观动作简述<br/>status: done|doing|paused|blocked <br/>summary: 核心结论摘要<br/>Timestamp: ISO8601"]
|
1. Standard node format: NodeID["stage name: brief macro action<br/>status: done|doing|paused|blocked <br/>summary: core conclusion summary<br/>Timestamp: ISO8601"]
|
||||||
2. 全员归宿映射:输入的每一个新 tool_call_id,都必须在 node_mapping 中被分配到一个 Node ID;MMD里的每一个node都应该有源头的tool_call消息来源,不能乱编,绝对不允许遗漏!(Node_id和tool_call_id是一对多的关系)
|
2. Full-coverage mapping: every single new tool_call_id in the input MUST be assigned to a Node ID in node_mapping; every node in the MMD must have an originating tool_call message source — never fabricate, and absolutely never omit any! (Node_id to tool_call_id is a one-to-many relationship.)
|
||||||
3. 你可以通过各种整合方法,尽量把更新后mmd文件大小控制在4000字以内
|
3. Use whatever consolidation methods you can to keep the updated mmd file under 4000 characters.
|
||||||
|
|
||||||
【严格时间戳与元数据规则】
|
[Strict Timestamp & Metadata Rules]
|
||||||
1. 顶部元数据(必填):%%{ "taskGoal": "一句话总结此次任务的目标(可动态更新)", "progress(0-100)": "进度百分比(严格点,几乎确认完成再打到90+)", createdTime": "ISO时间", "updatedTime": "ISO时间" }%%(updatedTime为node中的最新时间)。
|
1. Top metadata (required): %%{ "taskGoal": "one-sentence summary of this task's goal (may be updated dynamically)", "progress(0-100)": "progress percentage (be strict — only go 90+ when completion is almost certain)", createdTime": "ISO time", "updatedTime": "ISO time" }%% (updatedTime is the latest time among the nodes).
|
||||||
2. 节点内时间:如果合并了多个新条目,节点内的 Timestamp 必须取其中最新的 ISO 时间。
|
2. In-node time: if multiple new entries are merged into a node, the node's Timestamp must be the latest ISO time among them.
|
||||||
|
|
||||||
【严格 JSON 输出格式】
|
[Strict JSON Output Format]
|
||||||
务必正确转义双引号。所有 Mermaid 代码(无论是 mmd_content 还是 replace_blocks 中的 content)都必须用 \`\`\`mermaid ... \`\`\` 代码块包裹起来。必须输出如下 JSON 结构:
|
Escape double quotes correctly. All Mermaid code (whether mmd_content or the content in replace_blocks) must be wrapped in a \`\`\`mermaid ... \`\`\` code block. You must output the following JSON structure:
|
||||||
{
|
{
|
||||||
"file_action": "replace 或 write",
|
"file_action": "replace or write",
|
||||||
"mmd_content": "完整的、带转义的 .mmd 代码,必须用 \`\`\`mermaid ... \`\`\` 包裹。(仅在 file_action 为 write 时填写,否则必须设为 null)",
|
"mmd_content": "the complete, escaped .mmd code, wrapped in \`\`\`mermaid ... \`\`\`. (Fill this in only when file_action is write; otherwise it MUST be null)",
|
||||||
"replace_blocks": [
|
"replace_blocks": [
|
||||||
{
|
{
|
||||||
"start_line": "需要更新范围的起始行号(整数,对应 Existing Mermaid content 中的 L 标号)",
|
"start_line": "starting line number of the range to update (integer, matching the L labels in Existing Mermaid content)",
|
||||||
"end_line": "需要更新范围的结束行号(整数,包含该行)。要在某行之前插入新内容而不删除任何行,将 start_line 设为该行号,end_line 设为 start_line - 1",
|
"end_line": "ending line number of the range to update (integer, inclusive). To insert new content before a line without deleting any lines, set start_line to that line number and end_line to start_line - 1",
|
||||||
"content": "替换后的新内容(不需要带行号前缀),必须用 \`\`\`mermaid ... \`\`\` 包裹"
|
"content": "the new replacement content (no line-number prefixes), wrapped in \`\`\`mermaid ... \`\`\`"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_mapping": {
|
"node_mapping": {
|
||||||
@ -51,7 +51,7 @@ export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
仅输出纯 JSON 对象,绝不允许包含任何解释。`;
|
Output only the raw JSON object. Never include any explanation.`;
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -82,26 +82,26 @@ export function buildL2UserPrompt(opts: {
|
|||||||
|
|
||||||
// History section
|
// History section
|
||||||
if (recentHistory) {
|
if (recentHistory) {
|
||||||
parts.push(`## 近期对话历史:\n${recentHistory}`);
|
parts.push(`## Recent conversation history:\n${recentHistory}`);
|
||||||
} else {
|
} else {
|
||||||
parts.push("## 近期对话历史:\n(无可用历史)");
|
parts.push("## Recent conversation history:\n(no history available)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTurn) {
|
if (currentTurn) {
|
||||||
parts.push(`\n## 当前最新一轮:\n${currentTurn}`);
|
parts.push(`\n## Latest turn:\n${currentTurn}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(`\n## MMD prefix: ${mmdPrefix}`);
|
parts.push(`\n## MMD prefix: ${mmdPrefix}`);
|
||||||
parts.push(`(所有节点 ID 必须以此前缀开头,如 ${mmdPrefix}-N1, ${mmdPrefix}-N2...)`);
|
parts.push(`(All node IDs must start with this prefix, e.g. ${mmdPrefix}-N1, ${mmdPrefix}-N2...)`);
|
||||||
parts.push(`\n## Current task label: ${taskLabel}`);
|
parts.push(`\n## Current task label: ${taskLabel}`);
|
||||||
|
|
||||||
// Char count warning
|
// Char count warning
|
||||||
if (charCount > 2500) {
|
if (charCount > 2500) {
|
||||||
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
||||||
parts.push("⚠ 接近上限,请积极合并节点、精简 summary,优先使用 replace 模式微调而非 write 全量重写。");
|
parts.push("⚠ Approaching the limit — aggressively merge nodes and trim summaries; prefer replace-mode tweaks over full write rewrites.");
|
||||||
} else if (charCount > 2000) {
|
} else if (charCount > 2000) {
|
||||||
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
||||||
parts.push("注意控制增长,合并同类节点。");
|
parts.push("Watch the growth; merge similar nodes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing MMD with line numbers
|
// Existing MMD with line numbers
|
||||||
@ -122,6 +122,6 @@ export function buildL2UserPrompt(opts: {
|
|||||||
parts.push(`${i + 1}. [${e.toolCallId}] ${e.toolCall} → ${e.summary} (${e.timestamp})`);
|
parts.push(`${i + 1}. [${e.toolCallId}] ${e.toolCall} → ${e.summary} (${e.timestamp})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("\n请根据系统指令生成/更新 Mermaid 流程图,并输出合法的 JSON 对象(含 node_mapping)。");
|
parts.push("\nGenerate/update the Mermaid flowchart according to the system instructions, and output a valid JSON object (including node_mapping).");
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,27 +7,27 @@ const PARAMS_MAX_LEN = 500;
|
|||||||
const RESULT_MAX_LEN = 2000;
|
const RESULT_MAX_LEN = 2000;
|
||||||
const COMPRESS_THRESHOLD = 200;
|
const COMPRESS_THRESHOLD = 200;
|
||||||
|
|
||||||
export const L1_SYSTEM_PROMPT = `你是一个专为 AI 编码助手提供支持的"工具结果摘要器"。你的核心任务是深度理解当前的对话上下文,并将繁杂的工具调用与执行结果(一对toolcall和tool result整合成一条summary输出),提炼为高信息密度的 JSON 数组。
|
export const L1_SYSTEM_PROMPT = `You are a "tool result summarizer" built to support an AI coding assistant. Your core task is to deeply understand the current conversation context and distill verbose tool calls and their execution results (each toolcall/tool result pair is consolidated into one summary output) into a high-information-density JSON array.
|
||||||
|
|
||||||
在生成摘要前,请务必进行以下内部思考:
|
Before producing summaries, always perform the following internal reasoning:
|
||||||
1. 任务对齐:结合最近的对话记录,识别用户当前的核心目标和最新意图。若上下文存在冲突,始终以最新的用户意图为准。
|
1. Task alignment: use the recent conversation records to identify the user's current core goal and latest intent. If the context conflicts, always defer to the most recent user intent.
|
||||||
2. 价值过滤:忽略工具如何工作的冗余细节,直接提取"发现了什么关键线索"、"做了什么关键动作"、"修改了什么具体内容"或"遇到了什么具体报错"。
|
2. Value filtering: ignore redundant details about how the tool works; directly extract "what key clue was discovered", "what key action was taken", "what specific content was modified", or "what specific error was encountered".
|
||||||
3. 影响评估:判断该结果对当前任务的实质性影响(例如:证实了某个假设、推进了哪一步、做出了什么决策,或因为什么报错导致了阻塞)。
|
3. Impact assessment: judge the result's substantive impact on the current task (e.g., it confirmed a hypothesis, advanced a particular step, led to a decision, or caused a blockage due to a specific error).
|
||||||
|
|
||||||
【输出格式要求】
|
[Output Format Requirements]
|
||||||
你必须且只能输出一个合法的 JSON 对象数组 [{...}],每个对象**必须**包含以下字段:
|
You must output one and only one valid JSON array of objects [{...}]; each object MUST contain the following fields:
|
||||||
- "tool_call": 工具调用的简洁描述。处理规则如下:
|
- "tool_call": a concise description of the tool call. Handling rules:
|
||||||
· 如果输入中该 tool pair 标记了 [NEEDS_COMPRESS],你必须将工具名+关键参数压缩为一句简洁的描述(≤150字符),保留工具名、操作目标(如文件路径、命令意图),省略内联脚本/大段内容的细节。
|
· If the tool pair in the input is marked [NEEDS_COMPRESS], you must compress the tool name + key parameters into a single concise description (≤150 characters), keeping the tool name and the operation target (e.g., file path, command intent), while omitting the details of inline scripts / large content blocks.
|
||||||
示例:exec({"command":"python3 -c 'import csv; ...200行脚本...'"}) → "exec: 运行 Python (xx/xx/xx.sh,标明具体路径和文件)脚本分析 sales_channels.csv 数据质量"
|
Example: exec({"command":"python3 -c 'import csv; ...200-line script...'"}) → "exec: run a Python script (xx/xx/xx.sh — state the specific path and file) to analyze data quality of sales_channels.csv"
|
||||||
示例:write_file({"path":"/root/app.py","content":"...5000字符..."}) → "write_file: 写入 /root/app.py (Flask 应用主文件),大致内容是……"
|
Example: write_file({"path":"/root/app.py","content":"...5000 chars..."}) → "write_file: write /root/app.py (Flask app main file); its rough contents are ..."
|
||||||
· 如果未标记 [NEEDS_COMPRESS],直接简述工具与参数即可(系统会用原始值覆盖)。
|
· If not marked [NEEDS_COMPRESS], just briefly describe the tool and its parameters (the system will overwrite this with the original value).
|
||||||
- "summary": 融合上述思考的精炼总结(≤200个字符)。必须一针见血地说清楚结果的业务价值,以及它对任务的推进/阻塞作用。
|
- "summary": a distilled summary that fuses the reasoning above (≤200 characters). It must state, incisively, the business value of the result and how it advances or blocks the task.
|
||||||
- "tool_call_id": 原始的 tool_call_id(必须原样透传)。
|
- "tool_call_id": the original tool_call_id (must be passed through verbatim).
|
||||||
- "timestamp": 原始的 ISO 8601 时间戳(必须原样透传)。
|
- "timestamp": the original ISO 8601 timestamp (must be passed through verbatim).
|
||||||
- "score"(**必填**): 结合信息密度和任务目的分析summary对于原文的可替代性,范围在0-10之间,越接近10表示summary越能替代原文。
|
- "score" (REQUIRED): considering information density and the task's purpose, rate how well the summary can substitute for the original text, on a scale of 0-10 — the closer to 10, the better the summary can replace the original.
|
||||||
|
|
||||||
【严格规则】
|
[Strict Rules]
|
||||||
只允许输出纯 JSON 数组,严禁输出思考过程或其他解释性文本。`;
|
Output only the raw JSON array. Never output your reasoning process or any other explanatory text.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the L1 user prompt for summarization.
|
* Build the L1 user prompt for summarization.
|
||||||
@ -38,8 +38,8 @@ export function buildL1UserPrompt(
|
|||||||
): string {
|
): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
parts.push("## 最近的对话上下文(用于理解当前任务):");
|
parts.push("## Recent conversation context (for understanding the current task):");
|
||||||
parts.push(recentContext || "(无可用上下文)");
|
parts.push(recentContext || "(no context available)");
|
||||||
parts.push("\n## Tool call/result pairs to summarize:");
|
parts.push("\n## Tool call/result pairs to summarize:");
|
||||||
|
|
||||||
for (let i = 0; i < pairs.length; i++) {
|
for (let i = 0; i < pairs.length; i++) {
|
||||||
|
|||||||
@ -3,25 +3,25 @@
|
|||||||
*/
|
*/
|
||||||
import type { MmdMeta } from "../types.js";
|
import type { MmdMeta } from "../types.js";
|
||||||
|
|
||||||
export const L15_SYSTEM_PROMPT = `你是一个面向 AI 编码助手的"任务生命周期门神"。
|
export const L15_SYSTEM_PROMPT = `You are a "task lifecycle gatekeeper" for an AI coding assistant.
|
||||||
你的职责是交叉分析提供的三个输入源,精准研判任务状态,并输出纯 JSON 对象。
|
Your duty is to cross-analyze the three provided input sources, precisely assess the task state, and output a raw JSON object.
|
||||||
|
|
||||||
【输入数据利用指南(必须遵循的思考链路)】
|
[Input Usage Guide (three-step reasoning chain you MUST follow)]
|
||||||
1. 第一步 - 剖析 recentMessages(识别意图):根据当前和历史对话,提取用户最新回复的核心诉求。判断是"继续排查"、"宣布完工(如:跑通了)"、"单轮闲聊问答"还是"开启全新需求"。
|
1. Step 1 - Dissect recentMessages (identify intent): based on the current and past conversation, extract the core request of the user's latest reply. Decide whether it is "continuing the investigation", "declaring the work done (e.g., 'it runs now')", "a single-turn casual Q&A", or "starting a brand-new requirement".
|
||||||
2. 第二步 - 对齐 currentMmd(评估当前基线):将用户的最新意图与 currentMmd 的完整 Mermaid 内容进行比对——关注 taskGoal、各节点的 status(done/doing/todo)以及 summary。如果诉求完全超出了当前图表的范畴或目标已实现(所有节点 done 且无后续),则 taskCompleted 为 true。若仍在解决图表中的子问题(包括 doing 节点或修 bug),则为 false。(如果没有currentMmd,就只根据当前对话和历史对话来判断是否继续任务)
|
2. Step 2 - Align with currentMmd (assess the current baseline): compare the user's latest intent against the full Mermaid content of currentMmd — focus on taskGoal, each node's status (done/doing/todo), and the summaries. If the request falls entirely outside the diagram's scope, or the goal has already been achieved (all nodes done with no follow-up), then taskCompleted is true. If the user is still working on a sub-problem within the diagram (including doing nodes or bug fixing), it is false. (If there is no currentMmd, judge whether the task continues solely from the current and past conversation.)
|
||||||
3. 第三步 - 检索 availableMmds(判断是否延续):如果判定要开启新任务(isLongTask=true 且 taskCompleted=true/当前无任务),必须扫描 availableMmds 的 taskGoal 和时间信息。若新诉求与列表中某个旧任务高度重合(如回到昨天没做完的模块),则是延续(isContinuation=true)。
|
3. Step 3 - Search availableMmds (decide continuation): if you determine a new task is starting (isLongTask=true and taskCompleted=true / there is no current task), you MUST scan the taskGoal and time information of availableMmds. If the new request heavily overlaps with an old task in the list (e.g., returning to yesterday's unfinished module), it is a continuation (isContinuation=true).
|
||||||
|
|
||||||
【严格 JSON 输出格式】
|
[Strict JSON Output Format]
|
||||||
务必输出合法的纯 JSON 对象,格式如下:
|
Always output a valid raw JSON object in the following format:
|
||||||
{
|
{
|
||||||
"taskCompleted": boolean, // 当前任务是否已结束(如果 currentMmd 为 none,这里必须填 true)
|
"taskCompleted": boolean, // whether the current task has ended (if currentMmd is none, this MUST be true)
|
||||||
"isLongTask": boolean, // 最新诉求是否是需要多步操作的复杂工程(普通技术问答、闲聊填 false)
|
"isLongTask": boolean, // whether the latest request is a complex, multi-step engineering effort (ordinary technical Q&A or chit-chat → false)
|
||||||
"isContinuation": boolean, // 是否在延续 availableMmds 中的历史任务
|
"isContinuation": boolean, // whether this continues a historical task from availableMmds
|
||||||
"continuationMmdFile": "string|null", // 若延续旧任务,精确填入 availableMmds 中的文件名(不含路径前缀),否则为 null
|
"continuationMmdFile": "string|null", // if continuing an old task, fill in the exact filename from availableMmds (without path prefix); otherwise null
|
||||||
"newTaskLabel": "string|null" // 若是全新长任务,生成简短标签(≤30字符,kebab-case,如 "refactor-api"),否则为 null
|
"newTaskLabel": "string|null" // if this is a brand-new long task, generate a short label (≤30 chars, kebab-case, e.g. "refactor-api"); otherwise null
|
||||||
}
|
}
|
||||||
|
|
||||||
只输出纯 JSON 对象,绝不允许包含解释文字。`;
|
Output only the raw JSON object. Never include any explanatory text.`;
|
||||||
|
|
||||||
export interface L15CurrentMmd {
|
export interface L15CurrentMmd {
|
||||||
filename: string;
|
filename: string;
|
||||||
@ -38,21 +38,21 @@ export function buildL15UserPrompt(
|
|||||||
): string {
|
): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
parts.push("## 1. 最近的对话上下文 (Recent messages):");
|
parts.push("## 1. Recent conversation context (Recent messages):");
|
||||||
parts.push(recentMessages);
|
parts.push(recentMessages);
|
||||||
parts.push("\n## 2. 当前挂载的任务图 (Active Mermaid — 完整内容):");
|
parts.push("\n## 2. Currently mounted task diagram (Active Mermaid — full content):");
|
||||||
|
|
||||||
if (currentMmd && currentMmd.filename) {
|
if (currentMmd && currentMmd.filename) {
|
||||||
parts.push(`**File:** ${currentMmd.filename}`);
|
parts.push(`**File:** ${currentMmd.filename}`);
|
||||||
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
|
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
|
||||||
} else {
|
} else {
|
||||||
parts.push("(none - 当前处于闲置状态,无活跃任务)");
|
parts.push("(none - currently idle, no active task)");
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("\n## 3. 历史可用的任务图 (Available Mermaid task files):");
|
parts.push("\n## 3. Historical task diagrams (Available Mermaid task files):");
|
||||||
|
|
||||||
if (metas.length === 0) {
|
if (metas.length === 0) {
|
||||||
parts.push("(none - 暂无历史长任务)");
|
parts.push("(none - no historical long tasks yet)");
|
||||||
} else {
|
} else {
|
||||||
for (const m of metas) {
|
for (const m of metas) {
|
||||||
const total = m.doneCount + m.doingCount + m.todoCount;
|
const total = m.doneCount + m.doingCount + m.todoCount;
|
||||||
@ -75,7 +75,7 @@ export function buildL15UserPrompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
parts.push(
|
parts.push(
|
||||||
"请严格根据系统指令的【三步思考链路】进行研判,并输出合法的 JSON 对象。",
|
"Assess strictly according to the [three-step reasoning chain] in the system instructions, and output a valid JSON object.",
|
||||||
);
|
);
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,43 +3,43 @@
|
|||||||
*/
|
*/
|
||||||
import type { OffloadEntry } from "../types.js";
|
import type { OffloadEntry } from "../types.js";
|
||||||
|
|
||||||
export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓扑架构师与视觉叙事者。
|
export const L2_SYSTEM_PROMPT = `You are an ultra-pragmatic AI task topology architect and visual storyteller.
|
||||||
你的核心逻辑是用尽量少的字符表达尽量多的信息,让LLM模型能看懂,不是为人类服务,尽量减少无用的视觉符号。任务是将底层工具调用记录,升维映射为一张高度语义化、表现力丰富且极度克制的 Mermaid (flowchart TD) 认知状态机。你要根据当前任务和意图,归纳"过去",要思考"未来"如何用这些已有的信息(你只需要记录已有信息,不需要写下一步规划)并标记"雷区"。保持图表的高度概括性。
|
Your core logic is to express as much information as possible in as few characters as possible, so that an LLM can understand it — this is not for human consumption — minimizing useless visual symbols. Your task is to lift low-level tool call records up into a highly semantic, richly expressive yet extremely restrained Mermaid (flowchart TD) cognitive state machine. Based on the current task and intent, you must summarize the "past", think about how the "future" can use this existing information (you only need to record existing information — do NOT write next-step plans), and flag the "minefields". Keep the diagram highly summarized.
|
||||||
|
|
||||||
【高阶认知与拓扑指南(你的自主权与极简原则)】
|
[High-Level Cognition & Topology Guide (your autonomy and minimalism principles)]
|
||||||
1. 弹性聚合:你拥有决定节点拆合的完全自主权。对于连续的、意图相同的常规动作(如连续查看多个文件以了解上下文),建议合并为一个宏观节点;,但保留关键转折点或重大发现为独立节点。图表必须保持宏观和克制,绝不事无巨细地记流水账。
|
1. Elastic aggregation: you have full autonomy over splitting and merging nodes. For consecutive routine actions with the same intent (e.g., viewing several files in a row to gather context), merge them into one macro node; but keep key turning points or major discoveries as standalone nodes. The diagram must stay macro-level and restrained — never a blow-by-blow activity log.
|
||||||
2. 认知墓碑 (防重蹈覆辙):遇到彻底走不通的死胡同或引发严重报错的废弃方案,可以建立警示节点(status: blocked)(如果是价值不高的fail信息则不需要记录)。
|
2. Cognitive tombstones (avoid repeating mistakes): for utterly dead-end paths or abandoned approaches that triggered serious errors, you may create warning nodes (status: blocked) (low-value failure info need not be recorded).
|
||||||
3. 结论导向的摘要:节点的 summary(注意:尽量小于150字)应聚焦于"得出了什么结论"或"发生了什么实质改变",而非罗列琐碎的数据或参数,记得保持极简原则。
|
3. Conclusion-oriented summaries: a node's summary (note: keep it under ~150 characters) should focus on "what conclusion was reached" or "what substantive change happened", rather than listing trivial data or parameters — remember the minimalism principle.
|
||||||
4. 要实事求是,你的任务是记录并归纳已经发生的事情,不是规划未来的具体操作,未发生的节点不要写,记录的已发生节点要有对应的消息来源(对应标注node_id)。
|
4. Be factual: your job is to record and summarize what has already happened, not to plan future concrete operations. Do not write nodes for things that have not happened, and every recorded node must have a corresponding message source (annotated via node_id).
|
||||||
【符号即语义:高维认知字典(你的核心武器)】为了极致压缩 Token 并为你下一步推理提供"认知锚点",请自由使用不同的mmd形状来代表不同的节点逻辑。让形状替你说话,省略冗余的文字描述。
|
[Symbols as Semantics: a high-dimensional cognitive dictionary (your core weapon)] To compress tokens to the extreme and give your next reasoning step "cognitive anchors", freely use different mmd shapes to represent different node logic. Let the shapes speak for you and omit redundant text descriptions.
|
||||||
|
|
||||||
【高度自由的拓扑与极简法则】
|
[Highly Free Topology & Minimalism Rules]
|
||||||
1. 语义浓缩:既然形状已经表达了"领域",你的 summary 必须极其精简(≤150字),如"发现死锁"、"依赖冲突"、"已修复"。
|
1. Semantic condensation: since the shapes already express the "domain", your summary must be extremely terse (≤150 chars), e.g. "deadlock found", "dependency conflict", "fixed".
|
||||||
2. 弹性拓扑:自主使用带标签的连线(-->|测试失败|)和虚线(-.->|参考|)来构建"依赖树"和"假设验证环"。不要记流水账。
|
2. Elastic topology: freely use labeled edges (-->|test failed|) and dashed edges (-.->|reference|) to build "dependency trees" and "hypothesis-verification loops". Do not keep an activity log.
|
||||||
3. 动态更新 (Token 极简):
|
3. Dynamic updates (token minimalism):
|
||||||
- replace (增量微调):仅修改现有节点的状态、时间戳、短文本或追加极少节点时。
|
- replace (incremental tweak): only when modifying existing nodes' status, timestamps, short text, or appending very few nodes.
|
||||||
- write (全量重写):逻辑大洗牌、重构图表或初始化时。
|
- write (full rewrite): for major logic reshuffles, diagram restructuring, or initialization.
|
||||||
注意:Existing Mermaid content 中每行开头都带有行号标记(如 "L1: ..."),这些行号仅供你在 replace 模式中引用,不是 MMD 内容的一部分。
|
Note: every line of the Existing Mermaid content starts with a line-number marker (e.g. "L1: ..."); these line numbers are only for your reference in replace mode and are NOT part of the MMD content.
|
||||||
|
|
||||||
【严格的工程底线】
|
[Strict Engineering Baselines]
|
||||||
1.节点标准格式:NodeID["阶段名: 宏观动作简述<br/>status: done|doing|paused|blocked <br/>summary: 核心结论摘要<br/>Timestamp: ISO8601"]
|
1. Standard node format: NodeID["stage name: brief macro action<br/>status: done|doing|paused|blocked <br/>summary: core conclusion summary<br/>Timestamp: ISO8601"]
|
||||||
2. 全员归宿映射:输入的每一个新 tool_call_id,都必须在 node_mapping 中被分配到一个 Node ID;MMD里的每一个node都应该有源头的tool_call消息来源,不能乱编,绝对不允许遗漏!(Node_id和tool_call_id是一对多的关系)
|
2. Full-coverage mapping: every single new tool_call_id in the input MUST be assigned to a Node ID in node_mapping; every node in the MMD must have an originating tool_call message source — never fabricate, and absolutely never omit any! (Node_id to tool_call_id is a one-to-many relationship.)
|
||||||
3. 你可以通过各种整合方法,尽量把更新后mmd文件大小控制在4000字以内
|
3. Use whatever consolidation methods you can to keep the updated mmd file under 4000 characters.
|
||||||
|
|
||||||
【严格时间戳与元数据规则】
|
[Strict Timestamp & Metadata Rules]
|
||||||
1. 顶部元数据(必填):%%{ "taskGoal": "一句话总结此次任务的目标(可动态更新)", "progress(0-100)": "进度百分比(严格点,几乎确认完成再打到90+)", createdTime": "ISO时间", "updatedTime": "ISO时间" }%%(updatedTime为node中的最新时间)。
|
1. Top metadata (required): %%{ "taskGoal": "one-sentence summary of this task's goal (may be updated dynamically)", "progress(0-100)": "progress percentage (be strict — only go 90+ when completion is almost certain)", createdTime": "ISO time", "updatedTime": "ISO time" }%% (updatedTime is the latest time among the nodes).
|
||||||
2. 节点内时间:如果合并了多个新条目,节点内的 Timestamp 必须取其中最新的 ISO 时间。
|
2. In-node time: if multiple new entries are merged into a node, the node's Timestamp must be the latest ISO time among them.
|
||||||
|
|
||||||
【严格 JSON 输出格式】
|
[Strict JSON Output Format]
|
||||||
务必正确转义双引号。所有 Mermaid 代码(无论是 mmd_content 还是 replace_blocks 中的 content)都必须用 \`\`\`mermaid ... \`\`\` 代码块包裹起来。必须输出如下 JSON 结构:
|
Escape double quotes correctly. All Mermaid code (whether mmd_content or the content in replace_blocks) must be wrapped in a \`\`\`mermaid ... \`\`\` code block. You must output the following JSON structure:
|
||||||
{
|
{
|
||||||
"file_action": "replace 或 write",
|
"file_action": "replace or write",
|
||||||
"mmd_content": "完整的、带转义的 .mmd 代码,必须用 \`\`\`mermaid ... \`\`\` 包裹。(仅在 file_action 为 write 时填写,否则必须设为 null)",
|
"mmd_content": "the complete, escaped .mmd code, wrapped in \`\`\`mermaid ... \`\`\`. (Fill this in only when file_action is write; otherwise it MUST be null)",
|
||||||
"replace_blocks": [
|
"replace_blocks": [
|
||||||
{
|
{
|
||||||
"start_line": "需要更新范围的起始行号(整数,对应 Existing Mermaid content 中的 L 标号)",
|
"start_line": "starting line number of the range to update (integer, matching the L labels in Existing Mermaid content)",
|
||||||
"end_line": "需要更新范围的结束行号(整数,包含该行)。要在某行之前插入新内容而不删除任何行,将 start_line 设为该行号,end_line 设为 start_line - 1",
|
"end_line": "ending line number of the range to update (integer, inclusive). To insert new content before a line without deleting any lines, set start_line to that line number and end_line to start_line - 1",
|
||||||
"content": "替换后的新内容(不需要带行号前缀),必须用 \`\`\`mermaid ... \`\`\` 包裹"
|
"content": "the new replacement content (no line-number prefixes), wrapped in \`\`\`mermaid ... \`\`\`"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_mapping": {
|
"node_mapping": {
|
||||||
@ -48,8 +48,8 @@ export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
注意:node_mapping 中的 Node ID 必须是 MMD 中实际使用的完整 ID(包含 MMD prefix,如 "001-N1"),不能只写短 ID(如 "N1")。
|
Note: Node IDs in node_mapping must be the full IDs actually used in the MMD (including the MMD prefix, e.g. "001-N1"), never just the short ID (e.g. "N1").
|
||||||
仅输出纯 JSON 对象,绝不允许包含任何解释。`;
|
Output only the raw JSON object. Never include any explanation.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the L2 user prompt for MMD generation.
|
* Build the L2 user prompt for MMD generation.
|
||||||
@ -68,26 +68,26 @@ export function buildL2UserPrompt(opts: {
|
|||||||
|
|
||||||
// History section
|
// History section
|
||||||
if (recentHistory) {
|
if (recentHistory) {
|
||||||
parts.push(`## 近期对话历史:\n${recentHistory}`);
|
parts.push(`## Recent conversation history:\n${recentHistory}`);
|
||||||
} else {
|
} else {
|
||||||
parts.push("## 近期对话历史:\n(无可用历史)");
|
parts.push("## Recent conversation history:\n(no history available)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTurn) {
|
if (currentTurn) {
|
||||||
parts.push(`\n## 当前最新一轮:\n${currentTurn}`);
|
parts.push(`\n## Latest turn:\n${currentTurn}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(`\n## MMD prefix: ${mmdPrefix}`);
|
parts.push(`\n## MMD prefix: ${mmdPrefix}`);
|
||||||
parts.push(`(所有节点 ID 必须以此前缀开头,如 ${mmdPrefix}-N1, ${mmdPrefix}-N2...)`);
|
parts.push(`(All node IDs must start with this prefix, e.g. ${mmdPrefix}-N1, ${mmdPrefix}-N2...)`);
|
||||||
parts.push(`\n## Current task label: ${taskLabel}`);
|
parts.push(`\n## Current task label: ${taskLabel}`);
|
||||||
|
|
||||||
// Char count warning
|
// Char count warning
|
||||||
if (charCount > 2500) {
|
if (charCount > 2500) {
|
||||||
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
||||||
parts.push("⚠ 接近上限,请积极合并节点、精简 summary,优先使用 replace 模式微调而非 write 全量重写。");
|
parts.push("⚠ Approaching the limit — aggressively merge nodes and trim summaries; prefer replace-mode tweaks over full write rewrites.");
|
||||||
} else if (charCount > 2000) {
|
} else if (charCount > 2000) {
|
||||||
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
parts.push(`\n## Current MMD size: ${charCount} chars (budget: 4000 chars)`);
|
||||||
parts.push("注意控制增长,合并同类节点。");
|
parts.push("Watch the growth; merge similar nodes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing MMD with line numbers
|
// Existing MMD with line numbers
|
||||||
@ -108,6 +108,6 @@ export function buildL2UserPrompt(opts: {
|
|||||||
parts.push(`${i + 1}. [${e.tool_call_id}] ${e.tool_call} → ${e.summary} (${e.timestamp})`);
|
parts.push(`${i + 1}. [${e.tool_call_id}] ${e.tool_call} → ${e.summary} (${e.timestamp})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("\n请根据系统指令生成/更新 Mermaid 流程图,并输出合法的 JSON 对象(含 node_mapping)。");
|
parts.push("\nGenerate/update the Mermaid flowchart according to the system instructions, and output a valid JSON object (including node_mapping).");
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
9
MemoryKnowledge/docs/api/openapi.yaml
Normal file
9
MemoryKnowledge/docs/api/openapi.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# GODCALL stub — upstream's combined Dockerfile COPYs this path but the file is
|
||||||
|
# absent from the public repo at this commit. The knowledge server serves it as
|
||||||
|
# the Swagger document; regenerate/replace when upstream restores the real spec.
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: GODCALL Knowledge API
|
||||||
|
description: Wiki + CodeGraph knowledge service (self-hosted GODCALL fork).
|
||||||
|
version: 0.1.0
|
||||||
|
paths: {}
|
||||||
@ -81,7 +81,7 @@ export async function mergePage(
|
|||||||
// locked → 跳过,保护用户手工编辑。
|
// locked → 跳过,保护用户手工编辑。
|
||||||
const oldParsed = parseFrontmatter(existingContent);
|
const oldParsed = parseFrontmatter(existingContent);
|
||||||
if (oldParsed.frontmatter.locked === true) {
|
if (oldParsed.frontmatter.locked === true) {
|
||||||
return { action: "skip", reason: "目标页 locked,跳过合并" };
|
return { action: "skip", reason: "target page is locked; skipping merge" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const candParsed = parseFrontmatter(candidateContent);
|
const candParsed = parseFrontmatter(candidateContent);
|
||||||
|
|||||||
@ -87,7 +87,7 @@ export async function generateOverview(projectPath: string, llm: LlmClient): Pro
|
|||||||
const wikiDir = join(projectPath, "wiki");
|
const wikiDir = join(projectPath, "wiki");
|
||||||
const briefs = collectBriefs(wikiDir);
|
const briefs = collectBriefs(wikiDir);
|
||||||
if (briefs.length < 2) {
|
if (briefs.length < 2) {
|
||||||
log.debug("页面太少,跳过 overview 生成", { pages: briefs.length });
|
log.debug("too few pages, skipping overview generation", { pages: briefs.length });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export interface PageForUpdate {
|
|||||||
function formatExistingPages(existingPages: ExistingPageInfo[]): string {
|
function formatExistingPages(existingPages: ExistingPageInfo[]): string {
|
||||||
return existingPages.length > 0
|
return existingPages.length > 0
|
||||||
? existingPages
|
? existingPages
|
||||||
.map((p) => `- [${p.type}] ${p.relPath}${p.title ? ` — ${p.title}` : ""}${p.description ? `(${p.description})` : ""}`)
|
.map((p) => `- [${p.type}] ${p.relPath}${p.title ? ` — ${p.title}` : ""}${p.description ? ` (${p.description})` : ""}`)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: "(wiki is empty — this is the first source)";
|
: "(wiki is empty — this is the first source)";
|
||||||
}
|
}
|
||||||
@ -164,7 +164,7 @@ export function buildGeneratePrompt(args: {
|
|||||||
const existingList =
|
const existingList =
|
||||||
existingPages.length > 0
|
existingPages.length > 0
|
||||||
? existingPages
|
? existingPages
|
||||||
.map((p) => `- [${p.type}] ${p.relPath}${p.title ? ` — ${p.title}` : ""}${p.description ? `(${p.description})` : ""}`)
|
.map((p) => `- [${p.type}] ${p.relPath}${p.title ? ` — ${p.title}` : ""}${p.description ? ` (${p.description})` : ""}`)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: "(wiki is empty — this is the first source)";
|
: "(wiki is empty — this is the first source)";
|
||||||
|
|
||||||
|
|||||||
@ -55,10 +55,18 @@ export class GitSourceFetcher implements ISourceFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate(sourceUrl: string): void {
|
validate(sourceUrl: string): void {
|
||||||
// 第一版:仅支持 public HTTPS 仓库(SSH / 私有仓库鉴权见文档 005)。
|
// GODCALL: plain http:// is allowed when KNOWLEDGE_ALLOW_HTTP is set —
|
||||||
if (!sourceUrl.startsWith("https://")) {
|
// required for self-hosted Gitea reachable only over the private tailnet.
|
||||||
|
const allowHttp = /^(1|true|yes|on)$/i.test(
|
||||||
|
(process.env.KNOWLEDGE_ALLOW_HTTP ?? "").trim(),
|
||||||
|
);
|
||||||
|
const okProtocol =
|
||||||
|
sourceUrl.startsWith("https://") || (allowHttp && sourceUrl.startsWith("http://"));
|
||||||
|
if (!okProtocol) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"first version only supports public HTTPS repos; SSH/private repo support coming soon",
|
allowHttp
|
||||||
|
? "repo_url must be an http(s):// git URL"
|
||||||
|
: "only HTTPS repos are supported (set KNOWLEDGE_ALLOW_HTTP=1 to allow http://); SSH is not supported",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const host = this.extractHost(sourceUrl);
|
const host = this.extractHost(sourceUrl);
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Memory Hub</title>
|
<title>GODCALL</title>
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 226 KiB |
@ -28,7 +28,7 @@ export default function App() {
|
|||||||
if (auth === null) {
|
if (auth === null) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-[#0f172a]">
|
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-[#0f172a]">
|
||||||
<div className="text-sm text-slate-500 dark:text-slate-400">正在检测登录态…</div>
|
<div className="text-sm text-slate-500 dark:text-slate-400">Checking login status…</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -233,7 +233,7 @@ export default function LoginGate({
|
|||||||
// 并把下拉 placeholder 切成"加载失败",用户可刷新重试。
|
// 并把下拉 placeholder 切成"加载失败",用户可刷新重试。
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setInstancesError(true);
|
setInstancesError(true);
|
||||||
setError(`加载记忆实例列表失败,请刷新页面重试${err instanceof Error ? `(${err.message})` : ''}`);
|
setError(`Failed to load the memory instance list, please refresh the page and retry${err instanceof Error ? ` (${err.message})` : ''}`);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@ -243,12 +243,12 @@ export default function LoginGate({
|
|||||||
async function submit(e?: React.FormEvent) {
|
async function submit(e?: React.FormEvent) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (!instanceId) {
|
if (!instanceId) {
|
||||||
setError('请选择记忆实例。');
|
setError('Please select a memory instance.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = userKey.trim();
|
const key = userKey.trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
setError('请输入你的 user_key(sk-mem-…)。');
|
setError('Please enter your user_key (sk-mem-…).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -256,12 +256,12 @@ export default function LoginGate({
|
|||||||
try {
|
try {
|
||||||
const { valid, user } = await authVerifyApi.verify(instanceId, key);
|
const { valid, user } = await authVerifyApi.verify(instanceId, key);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
setError('user_key 无效或已吊销,请确认后重新输入。');
|
setError('user_key is invalid or has been revoked. Please check it and re-enter.');
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setError('登录响应缺少用户信息(data.user 为空),请联系后端确认 auth/verify 契约。');
|
setError('The login response is missing user information (data.user is empty). Please contact backend to confirm the auth/verify contract.');
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -285,17 +285,17 @@ export default function LoginGate({
|
|||||||
{/* ====== 左侧深色面板 ====== */}
|
{/* ====== 左侧深色面板 ====== */}
|
||||||
<div className="hidden lg:flex flex-col flex-1 bg-[#0b1120] relative overflow-hidden">
|
<div className="hidden lg:flex flex-col flex-1 bg-[#0b1120] relative overflow-hidden">
|
||||||
<div className="flex items-center gap-2.5 px-6 py-5">
|
<div className="flex items-center gap-2.5 px-6 py-5">
|
||||||
<img src="/logo.png" alt="Memory Hub" className="h-8 w-8" />
|
<img src="/logo.png" alt="GODCALL" className="h-8 w-8" />
|
||||||
<span className="text-[15px] font-semibold text-white/90 tracking-wide">Memory Hub</span>
|
<span className="text-[15px] font-semibold text-white/90 tracking-wide">GODCALL</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center px-8">
|
<div className="flex-1 flex flex-col items-center justify-center px-8">
|
||||||
<HeroIllustration />
|
<HeroIllustration />
|
||||||
<h2 className="mt-8 text-xl font-semibold text-white/90 tracking-wide">
|
<h2 className="mt-8 text-xl font-semibold text-white/90 tracking-wide">
|
||||||
TencentDB Memory Hub
|
GODCALL
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-slate-400 text-center max-w-xs">
|
<p className="mt-2 text-sm text-slate-400 text-center max-w-xs">
|
||||||
集中管理 Agent 的记忆、技能与知识资产
|
Centralized management of agent memory, skills, and knowledge assets
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -319,16 +319,16 @@ export default function LoginGate({
|
|||||||
{/* ====== 右侧登录表单面板 ====== */}
|
{/* ====== 右侧登录表单面板 ====== */}
|
||||||
<div className="w-full lg:w-[480px] xl:w-[520px] flex flex-col bg-white dark:bg-[#0f172a] overflow-y-auto">
|
<div className="w-full lg:w-[480px] xl:w-[520px] flex flex-col bg-white dark:bg-[#0f172a] overflow-y-auto">
|
||||||
<div className="flex lg:hidden items-center gap-2.5 px-6 py-4 border-b border-slate-200 dark:border-slate-700">
|
<div className="flex lg:hidden items-center gap-2.5 px-6 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
<img src="/logo.png" alt="Memory Hub" className="h-7 w-7" />
|
<img src="/logo.png" alt="GODCALL" className="h-7 w-7" />
|
||||||
<span className="text-[14px] font-semibold text-slate-800 dark:text-white/90">
|
<span className="text-[14px] font-semibold text-slate-800 dark:text-white/90">
|
||||||
Memory Hub
|
GODCALL
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col justify-center px-8 sm:px-12 lg:px-14 py-10">
|
<div className="flex-1 flex flex-col justify-center px-8 sm:px-12 lg:px-14 py-10">
|
||||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white/95">欢迎回来</h1>
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-white/95">Welcome back</h1>
|
||||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||||
请选择记忆实例并输入你的 user_key 登录。
|
Select a memory instance and enter your user_key to log in.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={submit} className="mt-8 _tdai-login-form">
|
<form onSubmit={submit} className="mt-8 _tdai-login-form">
|
||||||
@ -342,7 +342,7 @@ export default function LoginGate({
|
|||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
disabled={submitting || instances.length === 0}
|
disabled={submitting || instances.length === 0}
|
||||||
placeholder={instancesError ? '加载失败,请刷新重试' : '加载记忆实例中…'}
|
placeholder={instancesError ? 'Failed to load, please refresh and retry' : 'Loading memory instances…'}
|
||||||
options={instances.map((inst) => ({ value: inst.instance_id, text: inst.name }))}
|
options={instances.map((inst) => ({ value: inst.instance_id, text: inst.name }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -357,13 +357,13 @@ export default function LoginGate({
|
|||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
placeholder="user_key,如 sk-mem-xxxxxxxxxxxxxxxx"
|
placeholder="user_key, e.g. sk-mem-xxxxxxxxxxxxxxxx"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
rules={false}
|
rules={false}
|
||||||
/>
|
/>
|
||||||
<div className="_tdai-login-hint">
|
<div className="_tdai-login-hint">
|
||||||
请使用管理员为你分配的 user_key;若还没有,请联系团队管理员开号。
|
Use the user_key assigned to you by an administrator. If you don't have one yet, contact your team administrator to get an account set up.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -375,7 +375,7 @@ export default function LoginGate({
|
|||||||
loading={submitting}
|
loading={submitting}
|
||||||
disabled={submitting || !userKey.trim() || !instanceId}
|
disabled={submitting || !userKey.trim() || !instanceId}
|
||||||
>
|
>
|
||||||
{submitting ? '登录中…' : '登录'}
|
{submitting ? 'Logging in…' : 'Log in'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,29 +41,29 @@ const RESOURCE_MODULES: ResourceModule[] = [
|
|||||||
{
|
{
|
||||||
id: 'wiki',
|
id: 'wiki',
|
||||||
paramKey: 'llm_wiki.enabled',
|
paramKey: 'llm_wiki.enabled',
|
||||||
label: 'Wiki 知识库',
|
label: 'Wiki knowledge base',
|
||||||
desc: '关闭后仅停止工具注入',
|
desc: 'Disabling only stops tool injection',
|
||||||
icon: <BooksIcon size={16} />,
|
icon: <BooksIcon size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'code',
|
id: 'code',
|
||||||
paramKey: 'code_graph.enabled',
|
paramKey: 'code_graph.enabled',
|
||||||
label: 'Code_Graph',
|
label: 'Code_Graph',
|
||||||
desc: '关闭后仅停止工具注入',
|
desc: 'Disabling only stops tool injection',
|
||||||
icon: <CodeIcon size={16} />,
|
icon: <CodeIcon size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'skill',
|
id: 'skill',
|
||||||
paramKey: 'skill.enabled',
|
paramKey: 'skill.enabled',
|
||||||
label: 'Skill 技能',
|
label: 'Skill',
|
||||||
desc: '关闭后工具注入与新技能抽取均停止',
|
desc: 'Disabling stops both tool injection and new skill extraction',
|
||||||
icon: <ToolsIcon size={16} />,
|
icon: <ToolsIcon size={16} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chat_memory',
|
id: 'chat_memory',
|
||||||
paramKey: 'chat_memory.enabled',
|
paramKey: 'chat_memory.enabled',
|
||||||
label: 'Chat_Memory',
|
label: 'Chat_Memory',
|
||||||
desc: '关闭后工具注入与新对话写入均停止',
|
desc: 'Disabling stops both tool injection and new conversation writes',
|
||||||
icon: <ChatIcon size={16} />,
|
icon: <ChatIcon size={16} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -122,19 +122,19 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
await userConfigApi.setAssetCapability(mod.paramKey, next);
|
await userConfigApi.setAssetCapability(mod.paramKey, next);
|
||||||
tea.notify.success(`${mod.label} 已${next ? '开启' : '关闭'}`);
|
tea.notify.success(`${mod.label} has been ${next ? 'enabled' : 'disabled'}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setEnabled((prev) => ({ ...prev, [mod.id]: previous }));
|
setEnabled((prev) => ({ ...prev, [mod.id]: previous }));
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
setError(msg);
|
setError(msg);
|
||||||
tea.notify.error(`保存失败:${msg}`);
|
tea.notify.error(`Save failed: ${msg}`);
|
||||||
} finally {
|
} finally {
|
||||||
setSavingKey(null);
|
setSavingKey(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="设置 · 权限管理" size="m" onClose={onClose}>
|
<Modal visible caption="Settings · Permissions" size="m" onClose={onClose}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{/*
|
{/*
|
||||||
当前只有「权限管理」一个 tab,历史上用 <Tabs>+<TabPanel> 会渲染两条下划线
|
当前只有「权限管理」一个 tab,历史上用 <Tabs>+<TabPanel> 会渲染两条下划线
|
||||||
@ -146,13 +146,13 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ paddingTop: 4 }}>
|
<div style={{ paddingTop: 4 }}>
|
||||||
<Text theme="label" style={{ display: 'block', marginBottom: 8 }}>
|
<Text theme="label" style={{ display: 'block', marginBottom: 8 }}>
|
||||||
资源管理模块开关
|
Resource module toggles
|
||||||
</Text>
|
</Text>
|
||||||
<Text theme="weak" style={{ display: 'block', marginBottom: 16, fontSize: 12 }}>
|
<Text theme="weak" style={{ display: 'block', marginBottom: 16, fontSize: 12 }}>
|
||||||
开关按当前登录用户保存。关闭后,proxy 不会为该用户注入对应原子能力;变更对新会话即时生效。
|
Toggles are saved per logged-in user. When disabled, the proxy will not inject the corresponding atomic capability for this user; changes take effect immediately for new sessions.
|
||||||
</Text>
|
</Text>
|
||||||
{error && <Alert type="error" style={{ marginBottom: 12 }}>{error}</Alert>}
|
{error && <Alert type="error" style={{ marginBottom: 12 }}>{error}</Alert>}
|
||||||
{loading && <Alert type="info" style={{ marginBottom: 12 }}>正在读取当前用户资源配置…</Alert>}
|
{loading && <Alert type="info" style={{ marginBottom: 12 }}>Loading current user resource configuration…</Alert>}
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
{RESOURCE_MODULES.map((mod) => (
|
{RESOURCE_MODULES.map((mod) => (
|
||||||
@ -181,11 +181,11 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
|
|||||||
{mod.label}
|
{mod.label}
|
||||||
</Text>
|
</Text>
|
||||||
{savingKey === mod.paramKey ? (
|
{savingKey === mod.paramKey ? (
|
||||||
<Tag theme="warning" variant="soft" size="sm">保存中</Tag>
|
<Tag theme="warning" variant="soft" size="sm">Saving</Tag>
|
||||||
) : enabled[mod.id] ? (
|
) : enabled[mod.id] ? (
|
||||||
<Tag theme="success" variant="soft" size="sm">已开启</Tag>
|
<Tag theme="success" variant="soft" size="sm">Enabled</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag theme="default" variant="soft" size="sm">已关闭</Tag>
|
<Tag theme="default" variant="soft" size="sm">Disabled</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text theme="weak" style={{ fontSize: 12, marginTop: 2, display: 'block' }}>
|
<Text theme="weak" style={{ fontSize: 12, marginTop: 2, display: 'block' }}>
|
||||||
|
|||||||
@ -39,18 +39,18 @@ export interface PageMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PAGE_META: Record<PageId, PageMeta> = {
|
export const PAGE_META: Record<PageId, PageMeta> = {
|
||||||
workbench_board: { id: 'workbench_board', label: '任务看板', desc: 'Task 列表 / 创建 / 详情', group: '工作台', order: 0, affix: true },
|
workbench_board: { id: 'workbench_board', label: 'Task board', desc: 'Task list / create / details', group: 'Workbench', order: 0, affix: true },
|
||||||
wiki: { id: 'wiki', label: 'Wiki 知识库', desc: '来源 / 图谱 / 页面 / 搜索', group: '资产管理', order: 2 },
|
wiki: { id: 'wiki', label: 'Wiki knowledge base', desc: 'Sources / graph / pages / search', group: 'Assets', order: 2 },
|
||||||
code: { id: 'code', label: 'Code_Graph', desc: '仓库 / 索引 / 搜索 / 探索', group: '资产管理', order: 3 },
|
code: { id: 'code', label: 'Code_Graph', desc: 'Repos / index / search / explore', group: 'Assets', order: 3 },
|
||||||
skills: { id: 'skills', label: 'Skill 技能', desc: '全部 / 团队池 / Agent 资产', group: '资产管理', order: 4 },
|
skills: { id: 'skills', label: 'Skills', desc: 'All / team pool / agent assets', group: 'Assets', order: 4 },
|
||||||
chat_memory: { id: 'chat_memory', label: 'Chat_Memory', desc: 'L0–L3 分层记忆资产', group: '资产管理', order: 5 },
|
chat_memory: { id: 'chat_memory', label: 'Chat_Memory', desc: 'L0-L3 layered memory assets', group: 'Assets', order: 5 },
|
||||||
team_members: { id: 'team_members', label: '成员管理', desc: 'Team 成员 / 用户 / 角色', group: '组织与权限', order: 0 },
|
team_members: { id: 'team_members', label: 'Member management', desc: 'Team members / users / roles', group: 'Org & permissions', order: 0 },
|
||||||
team_agents: { id: 'team_agents', label: 'Agents 管理', desc: 'Agent / 可配置范围 / 固定资产', group: '组织与权限', order: 1 },
|
team_agents: { id: 'team_agents', label: 'Agent management', desc: 'Agents / configurable scope / fixed assets', group: 'Org & permissions', order: 1 },
|
||||||
api_keys: { id: 'api_keys', label: 'API Key', desc: '管理你的 API Key,用于外部客户端接入', group: '组织与权限', order: 2 },
|
api_keys: { id: 'api_keys', label: 'API Key', desc: 'Manage your API keys for external client access', group: 'Org & permissions', order: 2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 分组排序顺序 */
|
/** 分组排序顺序 */
|
||||||
export const GROUP_ORDER = ['工作台', '组织与权限', '资产管理'];
|
export const GROUP_ORDER = ['Workbench', 'Org & permissions', 'Assets'];
|
||||||
|
|
||||||
/** 每个页面在侧边栏菜单中的图标(Tea 官方图标,size 16) */
|
/** 每个页面在侧边栏菜单中的图标(Tea 官方图标,size 16) */
|
||||||
export const ITEM_ICON: Record<PageId, JSX.Element> = {
|
export const ITEM_ICON: Record<PageId, JSX.Element> = {
|
||||||
@ -66,7 +66,7 @@ export const ITEM_ICON: Record<PageId, JSX.Element> = {
|
|||||||
|
|
||||||
/** 分组图标(工作台 / 组织与权限 / 资产管理) */
|
/** 分组图标(工作台 / 组织与权限 / 资产管理) */
|
||||||
export const GROUP_ICON: Record<string, JSX.Element> = {
|
export const GROUP_ICON: Record<string, JSX.Element> = {
|
||||||
工作台: (
|
'Workbench': (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||||
@ -74,7 +74,7 @@ export const GROUP_ICON: Record<string, JSX.Element> = {
|
|||||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
组织与权限: (
|
'Org & permissions': (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
@ -82,7 +82,7 @@ export const GROUP_ICON: Record<string, JSX.Element> = {
|
|||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
资产管理: (
|
'Assets': (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M12 2l9 5-9 5-9-5 9-5z" />
|
<path d="M12 2l9 5-9 5-9-5 9-5z" />
|
||||||
<path d="M3 12l9 5 9-5" />
|
<path d="M3 12l9 5 9-5" />
|
||||||
|
|||||||
@ -107,7 +107,7 @@ export function ConsoleLayout() {
|
|||||||
|
|
||||||
for (const meta of Object.values(PAGE_META)) {
|
for (const meta of Object.values(PAGE_META)) {
|
||||||
// admin 角色 → 跳过所有「资源管理」分组下的项
|
// admin 角色 → 跳过所有「资源管理」分组下的项
|
||||||
if (userRole === 'admin' && meta.group === '资源管理') continue;
|
if (userRole === 'admin' && meta.group === 'Resource management') continue;
|
||||||
// reviewer → 跳过「成员管理」(member 可见,但新建/删除成员/Team 按钮在组件内按角色收敛)
|
// reviewer → 跳过「成员管理」(member 可见,但新建/删除成员/Team 按钮在组件内按角色收敛)
|
||||||
if (userRole === 'reviewer' && meta.id === 'team_members') continue;
|
if (userRole === 'reviewer' && meta.id === 'team_members') continue;
|
||||||
const list = byGroup.get(meta.group) ?? [];
|
const list = byGroup.get(meta.group) ?? [];
|
||||||
@ -124,8 +124,8 @@ export function ConsoleLayout() {
|
|||||||
}, [userRole]);
|
}, [userRole]);
|
||||||
|
|
||||||
// 「工作台」分组只有任务看板一项,置顶展示为独立入口,不显示分组标题
|
// 「工作台」分组只有任务看板一项,置顶展示为独立入口,不显示分组标题
|
||||||
const pinnedGroup = menuGroups.find((g) => g.title === '工作台');
|
const pinnedGroup = menuGroups.find((g) => g.title === 'Workbench');
|
||||||
const restGroups = menuGroups.filter((g) => g.title !== '工作台');
|
const restGroups = menuGroups.filter((g) => g.title !== 'Workbench');
|
||||||
|
|
||||||
const renderMenuItem = (item: PageMeta) => {
|
const renderMenuItem = (item: PageMeta) => {
|
||||||
const isActive = activePage === item.id;
|
const isActive = activePage === item.id;
|
||||||
|
|||||||
@ -68,14 +68,14 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="_memory-team-switcher-trigger"
|
className="_memory-team-switcher-trigger"
|
||||||
title={active?.name ?? '选择 team'}
|
title={active?.name ?? 'Select a team'}
|
||||||
>
|
>
|
||||||
<span className={`_memory-team-switcher-avatar ${active ? teamColor(active.team_id) : 'bg-primary'}`}>
|
<span className={`_memory-team-switcher-avatar ${active ? teamColor(active.team_id) : 'bg-primary'}`}>
|
||||||
{(active?.name ?? '?').slice(0, 1).toUpperCase()}
|
{(active?.name ?? '?').slice(0, 1).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<span className="_memory-team-switcher-meta">
|
<span className="_memory-team-switcher-meta">
|
||||||
<span className="_memory-team-switcher-name">{active?.name ?? '选择 team'}</span>
|
<span className="_memory-team-switcher-name">{active?.name ?? 'Select a team'}</span>
|
||||||
<span className="_memory-team-switcher-id">{active?.team_id ?? '未选择'}</span>
|
<span className="_memory-team-switcher-id">{active?.team_id ?? 'Not selected'}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon size={12} className="_memory-team-switcher-chevron" />
|
<ChevronDownIcon size={12} className="_memory-team-switcher-chevron" />
|
||||||
</button>
|
</button>
|
||||||
@ -84,20 +84,20 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
|
|||||||
{(close) => (
|
{(close) => (
|
||||||
<div className="_memory-team-switcher-panel">
|
<div className="_memory-team-switcher-panel">
|
||||||
<div className="_memory-team-switcher-panel-header">
|
<div className="_memory-team-switcher-panel-header">
|
||||||
<div className="_memory-team-switcher-panel-title">切换团队</div>
|
<div className="_memory-team-switcher-panel-title">Switch team</div>
|
||||||
<div className="_memory-team-switcher-panel-desc">
|
<div className="_memory-team-switcher-panel-desc">
|
||||||
不同团队的资产相互独立。切换后会在当前页面显示对应团队的数据。
|
Assets are independent across teams. Switching will show that team's data on the current page.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="_memory-team-switcher-panel-label">团队({myTeams.length})</div>
|
<div className="_memory-team-switcher-panel-label">Teams ({myTeams.length})</div>
|
||||||
|
|
||||||
<div className="_memory-team-switcher-list-wrap">
|
<div className="_memory-team-switcher-list-wrap">
|
||||||
{myTeams.length === 0 ? (
|
{myTeams.length === 0 ? (
|
||||||
<div className="_memory-team-switcher-empty">
|
<div className="_memory-team-switcher-empty">
|
||||||
{userRole === 'admin'
|
{userRole === 'admin'
|
||||||
? '暂无 team。点击下方「新建团队」创建。'
|
? 'No teams yet. Click "New team" below to create one.'
|
||||||
: '你还没有被加入任何 team。请联系管理员将你加入团队。'}
|
: "You haven't been added to any team yet. Contact an admin to be added."}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<List type="plain" split="divide" className="_memory-team-switcher-list">
|
<List type="plain" split="divide" className="_memory-team-switcher-list">
|
||||||
@ -115,7 +115,7 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="_memory-team-switcher-item-meta">
|
<span className="_memory-team-switcher-item-meta">
|
||||||
<span className="_memory-team-switcher-item-name">{t.name}</span>
|
<span className="_memory-team-switcher-item-name">{t.name}</span>
|
||||||
<span className="_memory-team-switcher-item-count">{t.members.length} 名成员</span>
|
<span className="_memory-team-switcher-item-count">{t.members.length} members</span>
|
||||||
</span>
|
</span>
|
||||||
{isActive && <CheckIcon size={16} className="_memory-team-switcher-item-check" />}
|
{isActive && <CheckIcon size={16} className="_memory-team-switcher-item-check" />}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
@ -133,22 +133,22 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
|
|||||||
size="full"
|
size="full"
|
||||||
value={newTeamName}
|
value={newTeamName}
|
||||||
onChange={setNewTeamName}
|
onChange={setNewTeamName}
|
||||||
placeholder="团队名称(必填)"
|
placeholder="Team name (required)"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={newTeamDesc}
|
value={newTeamDesc}
|
||||||
onChange={setNewTeamDesc}
|
onChange={setNewTeamDesc}
|
||||||
placeholder="团队描述(选填)"
|
placeholder="Team description (optional)"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-team-switcher-create-actions">
|
<div className="_memory-team-switcher-create-actions">
|
||||||
<Button onClick={resetCreateForm}>取消</Button>
|
<Button onClick={resetCreateForm}>Cancel</Button>
|
||||||
<Button type="primary"
|
<Button type="primary"
|
||||||
loading={creating}
|
loading={creating}
|
||||||
disabled={!newTeamName.trim() || creating}
|
disabled={!newTeamName.trim() || creating}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
>
|
>
|
||||||
创建
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -158,7 +158,7 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
|
|||||||
onClick={() => setShowCreateTeam(true)}
|
onClick={() => setShowCreateTeam(true)}
|
||||||
>
|
>
|
||||||
<AddIcon size={14} />
|
<AddIcon size={14} />
|
||||||
新建团队
|
New team
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,23 +31,23 @@ export function GlobalHeader({
|
|||||||
{/* 左侧:品牌 + 团队切换器 */}
|
{/* 左侧:品牌 + 团队切换器 */}
|
||||||
<div className="_memory-global-header-left">
|
<div className="_memory-global-header-left">
|
||||||
<div className="_memory-global-header-brand">
|
<div className="_memory-global-header-brand">
|
||||||
<img src="/logo.png" alt="Memory Hub" className="_memory-global-header-logo" />
|
<img src="/logo.png" alt="GODCALL" className="_memory-global-header-logo" />
|
||||||
<span className="_memory-global-header-brand-text">Memory Hub</span>
|
<span className="_memory-global-header-brand-text">GODCALL</span>
|
||||||
</div>
|
</div>
|
||||||
<TeamSwitcher userRole={userRole} />
|
<TeamSwitcher userRole={userRole} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:同步状态 + 用户菜单 */}
|
{/* 右侧:同步状态 + 用户菜单 */}
|
||||||
<div className="_memory-global-header-right">
|
<div className="_memory-global-header-right">
|
||||||
<span className="_memory-global-header-sync" title="实时同步已连接">
|
<span className="_memory-global-header-sync" title="Real-time sync connected">
|
||||||
<span className="_memory-global-header-sync-dot" />
|
<span className="_memory-global-header-sync-dot" />
|
||||||
实时同步
|
Live sync
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="_memory-global-header-icon-btn"
|
className="_memory-global-header-icon-btn"
|
||||||
title="设置"
|
title="Settings"
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
>
|
>
|
||||||
<SettingIcon size={16} />
|
<SettingIcon size={16} />
|
||||||
@ -72,7 +72,7 @@ export function GlobalHeader({
|
|||||||
setProfileOpen(true);
|
setProfileOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
我的资料
|
My profile
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item
|
<List.Item
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -80,7 +80,7 @@ export function GlobalHeader({
|
|||||||
onLogout();
|
onLogout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
退出登录
|
Log out
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
@ -88,18 +88,18 @@ export function GlobalHeader({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profileOpen && currentUserId && (
|
{profileOpen && currentUserId && (
|
||||||
<Modal visible caption="我的资料" size="s" onClose={() => setProfileOpen(false)}>
|
<Modal visible caption="My profile" size="s" onClose={() => setProfileOpen(false)}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<dl className="_memory-profile-details">
|
<dl className="_memory-profile-details">
|
||||||
<div><dt>用户名</dt><dd>{currentUser}</dd></div>
|
<div><dt>Username</dt><dd>{currentUser}</dd></div>
|
||||||
<div>
|
<div>
|
||||||
<dt>User ID</dt>
|
<dt>User ID</dt>
|
||||||
<dd><code>{currentUserId}</code> <Copy text={currentUserId} /></dd>
|
<dd><code>{currentUserId}</code> <Copy text={currentUserId} /></dd>
|
||||||
<small>发给团队管理员用于邀请你加入 Team</small>
|
<small>Share this with a team admin to be invited to a team</small>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer><Button onClick={() => setProfileOpen(false)}>关闭</Button></Modal.Footer>
|
<Modal.Footer><Button onClick={() => setProfileOpen(false)}>Close</Button></Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -5,69 +5,69 @@ export interface ErrorEnvelopeLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ERROR_CODE_MESSAGES: Record<string, string> = {
|
const ERROR_CODE_MESSAGES: Record<string, string> = {
|
||||||
UNAUTHORIZED: '登录状态已失效,请重新登录。',
|
UNAUTHORIZED: 'Your session has expired. Please log in again.',
|
||||||
INVALID_USER_KEY: '用户密钥无效或已失效,请重新登录。',
|
INVALID_USER_KEY: 'Your user key is invalid or has expired. Please log in again.',
|
||||||
MISSING_USER_KEY: '缺少用户密钥,请重新登录。',
|
MISSING_USER_KEY: 'Missing user key. Please log in again.',
|
||||||
MISSING_INSTANCE_ID: '缺少实例信息,请重新选择实例后重试。',
|
MISSING_INSTANCE_ID: 'Missing instance information. Please reselect an instance and retry.',
|
||||||
INVALID_INSTANCE: '实例配置无效,请检查当前选择的实例。',
|
INVALID_INSTANCE: 'Invalid instance configuration. Please check the currently selected instance.',
|
||||||
NOT_TEAM_MEMBER: '你不是该团队成员,无法执行此操作。',
|
NOT_TEAM_MEMBER: 'You are not a member of this team and cannot perform this action.',
|
||||||
PERMISSION_DENIED: '没有权限执行此操作。',
|
PERMISSION_DENIED: 'You do not have permission to perform this action.',
|
||||||
FORBIDDEN: '没有权限执行此操作。',
|
FORBIDDEN: 'You do not have permission to perform this action.',
|
||||||
NOT_FOUND: '资源不存在或已被删除。',
|
NOT_FOUND: 'The resource does not exist or has been deleted.',
|
||||||
ALREADY_EXISTS: '资源已存在,请勿重复创建。',
|
ALREADY_EXISTS: 'The resource already exists. Please do not create it again.',
|
||||||
MEMBER_ALREADY_EXISTS: '该用户已是团队成员,无需重复添加。',
|
MEMBER_ALREADY_EXISTS: 'This user is already a team member and does not need to be added again.',
|
||||||
CONFLICT: '资源状态已变化,请刷新后重试。',
|
CONFLICT: 'The resource state has changed. Please refresh and try again.',
|
||||||
KERNEL_UNAVAILABLE: '内核服务不可用,请稍后重试。',
|
KERNEL_UNAVAILABLE: 'The kernel service is unavailable. Please try again later.',
|
||||||
UPSTREAM_ERROR: '上游服务调用失败,请稍后重试。',
|
UPSTREAM_ERROR: 'The upstream service call failed. Please try again later.',
|
||||||
UNKNOWN_META_ACTION: '当前接口暂不支持,请刷新页面或联系管理员。',
|
UNKNOWN_META_ACTION: 'This endpoint is not currently supported. Please refresh the page or contact an administrator.',
|
||||||
NOT_IN_SCOPE: '该能力当前暂未开放。',
|
NOT_IN_SCOPE: 'This capability is not currently available.',
|
||||||
|
|
||||||
MISSING_TEAM_ID: '缺少团队信息,请重新选择团队。',
|
MISSING_TEAM_ID: 'Missing team information. Please reselect a team.',
|
||||||
MISSING_AGENT_ID: '缺少 Agent 信息,请重新选择 Agent。',
|
MISSING_AGENT_ID: 'Missing agent information. Please reselect an agent.',
|
||||||
AGENT_NOT_FOUND: 'Agent 不存在或已被删除。',
|
AGENT_NOT_FOUND: 'The agent does not exist or has been deleted.',
|
||||||
NOT_YOUR_AGENT: '只能操作你自己创建的 Agent。',
|
NOT_YOUR_AGENT: 'You can only operate on agents you created yourself.',
|
||||||
AGENT_NOT_IN_TEAM: '该 Agent 不属于当前团队。',
|
AGENT_NOT_IN_TEAM: 'This agent does not belong to the current team.',
|
||||||
MISSING_TASK_ID: '缺少 Task 信息,请重新选择 Task。',
|
MISSING_TASK_ID: 'Missing task information. Please reselect a task.',
|
||||||
|
|
||||||
MISSING_ASSET_ID: '缺少资产 ID。',
|
MISSING_ASSET_ID: 'Missing asset ID.',
|
||||||
ASSET_NOT_FOUND: '资产不存在或已被删除。',
|
ASSET_NOT_FOUND: 'The asset does not exist or has been deleted.',
|
||||||
ASSET_NOT_SHARED: '该资产尚未共享到团队,不能分配给其它 Agent。',
|
ASSET_NOT_SHARED: 'This asset has not been shared with the team yet and cannot be assigned to other agents.',
|
||||||
ASSET_TYPE_MISMATCH: '资产类型不匹配,请刷新后重试。',
|
ASSET_TYPE_MISMATCH: 'Asset type mismatch. Please refresh and try again.',
|
||||||
MISSING_BLOCK_ID: '缺少记忆资产信息。',
|
MISSING_BLOCK_ID: 'Missing memory asset information.',
|
||||||
BLOCK_NOT_FOUND: '记忆资产不存在或已被删除。',
|
BLOCK_NOT_FOUND: 'The memory asset does not exist or has been deleted.',
|
||||||
NOT_CHAT_MEMORY: '当前资产不是 Chat Memory。',
|
NOT_CHAT_MEMORY: 'This asset is not a Chat Memory.',
|
||||||
TEAM_MISMATCH: '资源不属于当前团队,请刷新后重试。',
|
TEAM_MISMATCH: 'The resource does not belong to the current team. Please refresh and try again.',
|
||||||
INVALID_SCOPE: '可见范围无效。',
|
INVALID_SCOPE: 'Invalid visibility scope.',
|
||||||
|
|
||||||
CANNOT_ALLOCATE_SELF_CHAT_MEMORY: '不能把该 Agent 自己的记忆再分配给自己。',
|
CANNOT_ALLOCATE_SELF_CHAT_MEMORY: 'You cannot assign this agent\'s own memory back to itself.',
|
||||||
CANNOT_UNBIND_SELF_CHAT_MEMORY: '不能解绑 Agent 自己的记忆。',
|
CANNOT_UNBIND_SELF_CHAT_MEMORY: 'You cannot unbind an agent\'s own memory.',
|
||||||
ALREADY_ALLOCATED: '这条资产已经分配给该 Agent,无需重复分配。',
|
ALREADY_ALLOCATED: 'This asset has already been assigned to this agent and does not need to be assigned again.',
|
||||||
IMPORT_LIMIT_EXCEEDED: '该 Agent 最多只能借入 2 条其它 Agent 的记忆。',
|
IMPORT_LIMIT_EXCEEDED: 'This agent can borrow at most 2 memories from other agents.',
|
||||||
// 内核 canBindAsset/permission-checker 判定失败:常见场景是 asset 被 owner 切私密后
|
// 内核 canBindAsset/permission-checker 判定失败:常见场景是 asset 被 owner 切私密后
|
||||||
// 其他成员再对它做 read / bind / update 类操作。
|
// 其他成员再对它做 read / bind / update 类操作。
|
||||||
ASSET_PRIVATE_INACCESSIBLE: '该资产已被 owner 设为私密,你无权访问。',
|
ASSET_PRIVATE_INACCESSIBLE: 'This asset has been set to private by its owner. You do not have access to it.',
|
||||||
ASSET_NOT_BINDABLE: '该资产的可见范围不允许绑定到此 Agent。请让 owner 将它设为团队可见后重试。',
|
ASSET_NOT_BINDABLE: 'This asset\'s visibility scope does not allow it to be bound to this agent. Please ask the owner to make it team-visible and try again.',
|
||||||
|
|
||||||
INVALID_TITLE: '标题不能为空且不能超过长度限制。',
|
INVALID_TITLE: 'The title cannot be empty and must not exceed the length limit.',
|
||||||
MISSING_MESSAGES: '缺少对话消息。',
|
MISSING_MESSAGES: 'Missing conversation messages.',
|
||||||
TOO_MANY_MESSAGES: '一次最多导入 100 条消息。',
|
TOO_MANY_MESSAGES: 'You can import at most 100 messages at a time.',
|
||||||
NO_VALID_MESSAGES: '没有可导入的有效消息。',
|
NO_VALID_MESSAGES: 'There are no valid messages to import.',
|
||||||
|
|
||||||
MISSING_WIKI_ID: '缺少 Wiki 信息。',
|
MISSING_WIKI_ID: 'Missing wiki information.',
|
||||||
WIKI_NOT_FOUND: 'Wiki 不存在或已被删除。',
|
WIKI_NOT_FOUND: 'The wiki does not exist or has been deleted.',
|
||||||
WIKI_EMPTY_NO_SOURCES: 'Wiki 还没有上传源文件,请先上传 .md 文件后再抽取。',
|
WIKI_EMPTY_NO_SOURCES: 'This wiki has no uploaded source files yet. Please upload .md files before extracting.',
|
||||||
MISSING_FILES: '请至少上传一个文件。',
|
MISSING_FILES: 'Please upload at least one file.',
|
||||||
TOO_MANY_FILES: '上传文件数量超过限制(最多 10 个),请分批上传。',
|
TOO_MANY_FILES: 'Too many files uploaded (max 10). Please upload in batches.',
|
||||||
FILE_TOO_LARGE: '单个文件不能超过 512KB,请精简后再上传。',
|
FILE_TOO_LARGE: 'A single file cannot exceed 512KB. Please trim it down before uploading.',
|
||||||
TOTAL_TOO_LARGE: '单次上传总量不能超过 5MB,请分批上传。',
|
TOTAL_TOO_LARGE: 'The total upload size cannot exceed 5MB. Please upload in batches.',
|
||||||
MISSING_CODE_GRAPH_ID: '缺少 CodeGraph 信息。',
|
MISSING_CODE_GRAPH_ID: 'Missing CodeGraph information.',
|
||||||
CODE_GRAPH_NOT_FOUND: 'CodeGraph 不存在或已被删除。',
|
CODE_GRAPH_NOT_FOUND: 'The CodeGraph does not exist or has been deleted.',
|
||||||
KNOWLEDGE_NOT_FOUND: '知识库资源不存在或已被删除。',
|
KNOWLEDGE_NOT_FOUND: 'The knowledge base resource does not exist or has been deleted.',
|
||||||
|
|
||||||
INVALID_ARGUMENT: '请求参数不正确,请检查输入后重试。',
|
INVALID_ARGUMENT: 'The request parameters are invalid. Please check your input and try again.',
|
||||||
VALIDATION_ERROR: '请求参数不正确,请检查输入后重试。',
|
VALIDATION_ERROR: 'The request parameters are invalid. Please check your input and try again.',
|
||||||
RATE_LIMITED: '请求过于频繁,请稍后重试。',
|
RATE_LIMITED: 'Too many requests. Please try again later.',
|
||||||
INTERNAL_ERROR: '服务内部错误,请稍后重试。',
|
INTERNAL_ERROR: 'Internal server error. Please try again later.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MESSAGE_PATTERNS: Array<[RegExp, string]> = [
|
const MESSAGE_PATTERNS: Array<[RegExp, string]> = [
|
||||||
@ -79,9 +79,9 @@ const MESSAGE_PATTERNS: Array<[RegExp, string]> = [
|
|||||||
// 注:asset_not_bindable / visibility_restricted 在 PRIORITY_MESSAGE_PATTERNS 里前置匹配,
|
// 注:asset_not_bindable / visibility_restricted 在 PRIORITY_MESSAGE_PATTERNS 里前置匹配,
|
||||||
// 因为它们会被 permission_denied 前缀吞掉。
|
// 因为它们会被 permission_denied 前缀吞掉。
|
||||||
[/permission[_\s-]?denied/i, ERROR_CODE_MESSAGES.PERMISSION_DENIED],
|
[/permission[_\s-]?denied/i, ERROR_CODE_MESSAGES.PERMISSION_DENIED],
|
||||||
[/fetch failed|networkerror|failed to fetch/i, '网络请求失败,请检查服务是否可用后重试。'],
|
[/fetch failed|networkerror|failed to fetch/i, 'Network request failed. Please check whether the service is available and try again.'],
|
||||||
[/timeout|aborted/i, '请求超时,请稍后重试。'],
|
[/timeout|aborted/i, 'The request timed out. Please try again later.'],
|
||||||
[/empty .* response/i, '服务返回为空,请稍后重试。'],
|
[/empty .* response/i, 'The service returned an empty response. Please try again later.'],
|
||||||
[/internal server error/i, ERROR_CODE_MESSAGES.INTERNAL_ERROR],
|
[/internal server error/i, ERROR_CODE_MESSAGES.INTERNAL_ERROR],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -182,7 +182,7 @@ export function formatApiErrorMessage(input: {
|
|||||||
if (input.httpStatus === 403) return ERROR_CODE_MESSAGES.PERMISSION_DENIED;
|
if (input.httpStatus === 403) return ERROR_CODE_MESSAGES.PERMISSION_DENIED;
|
||||||
if (input.httpStatus === 404) return ERROR_CODE_MESSAGES.NOT_FOUND;
|
if (input.httpStatus === 404) return ERROR_CODE_MESSAGES.NOT_FOUND;
|
||||||
if (input.httpStatus && input.httpStatus >= 500) return ERROR_CODE_MESSAGES.INTERNAL_ERROR;
|
if (input.httpStatus && input.httpStatus >= 500) return ERROR_CODE_MESSAGES.INTERNAL_ERROR;
|
||||||
return input.fallback ?? '操作失败,请稍后重试。';
|
return input.fallback ?? 'Operation failed. Please try again later.';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getErrorMessage(err: unknown): string {
|
export function getErrorMessage(err: unknown): string {
|
||||||
@ -207,5 +207,5 @@ export function getErrorMessage(err: unknown): string {
|
|||||||
}
|
}
|
||||||
if (err instanceof Error) return formatApiErrorMessage({ message: err.message, fallback: err.message });
|
if (err instanceof Error) return formatApiErrorMessage({ message: err.message, fallback: err.message });
|
||||||
if (typeof err === 'string') return formatApiErrorMessage({ message: err, fallback: err });
|
if (typeof err === 'string') return formatApiErrorMessage({ message: err, fallback: err });
|
||||||
return '操作失败,请稍后重试。';
|
return 'Operation failed. Please try again later.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,17 +258,17 @@ async function listAgentFixedKnowledge(agentId: string): Promise<KnowledgeFixedI
|
|||||||
// ========================= Wiki API =========================
|
// ========================= Wiki API =========================
|
||||||
|
|
||||||
export function wikiStageLabel(status: WikiDetail['status'], internalStatus?: string | null): string {
|
export function wikiStageLabel(status: WikiDetail['status'], internalStatus?: string | null): string {
|
||||||
if (status === 'missing') return '已丢失';
|
if (status === 'missing') return 'Lost';
|
||||||
if (status === 'pending') return '排队中';
|
if (status === 'pending') return 'Queued';
|
||||||
if (status === 'ready') return '已完成';
|
if (status === 'ready') return 'Completed';
|
||||||
if (status === 'failed') return '失败';
|
if (status === 'failed') return 'Failed';
|
||||||
if (status === 'draft') return '待加工';
|
if (status === 'draft') return 'Awaiting processing';
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
scanning: '扫描源文档',
|
scanning: 'Scanning source documents',
|
||||||
ingesting: '抽取文档内容',
|
ingesting: 'Extracting document content',
|
||||||
'rebuilding-index': '重建索引',
|
'rebuilding-index': 'Rebuilding index',
|
||||||
};
|
};
|
||||||
return internalStatus ? (map[internalStatus] ?? internalStatus) : '加工中';
|
return internalStatus ? (map[internalStatus] ?? internalStatus) : 'Processing';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wikiProgressPercent(status: WikiDetail['status'], internalStatus?: string | null): number {
|
export function wikiProgressPercent(status: WikiDetail['status'], internalStatus?: string | null): number {
|
||||||
@ -321,7 +321,7 @@ export const knowledgeApi = {
|
|||||||
/** 触发 ingest 后轮询 wiki/get,用真实 status/internal_status 驱动进度展示。 */
|
/** 触发 ingest 后轮询 wiki/get,用真实 status/internal_status 驱动进度展示。 */
|
||||||
ingestWithPolling: async (wikiId: string, callbacks: IngestStreamCallbacks, _teamId: string): Promise<void> => {
|
ingestWithPolling: async (wikiId: string, callbacks: IngestStreamCallbacks, _teamId: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
callbacks.onProgress?.({ type: 'file_start', detail: '正在触发抽取...', done: 0, total: 100, ts: Date.now() });
|
callbacks.onProgress?.({ type: 'file_start', detail: 'Triggering extraction...', done: 0, total: 100, ts: Date.now() });
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.ingest(wikiId);
|
await knowledgeApi.wiki.ingest(wikiId);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -335,27 +335,27 @@ export const knowledgeApi = {
|
|||||||
const detail = await knowledgeApi.wiki.get(wikiId);
|
const detail = await knowledgeApi.wiki.get(wikiId);
|
||||||
const stage = wikiStageLabel(detail.status, detail.internal_status);
|
const stage = wikiStageLabel(detail.status, detail.internal_status);
|
||||||
const done = wikiProgressPercent(detail.status, detail.internal_status);
|
const done = wikiProgressPercent(detail.status, detail.internal_status);
|
||||||
const pageHint = typeof detail.page_count === 'number' ? `,当前 ${detail.page_count} 页` : '';
|
const pageHint = typeof detail.page_count === 'number' ? `, ${detail.page_count} pages so far` : '';
|
||||||
callbacks.onProgress?.({
|
callbacks.onProgress?.({
|
||||||
type: 'file_done',
|
type: 'file_done',
|
||||||
detail: `第 ${attempt} 次检查:${stage}${pageHint}`,
|
detail: `Check ${attempt}: ${stage}${pageHint}`,
|
||||||
done,
|
done,
|
||||||
total: 100,
|
total: 100,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (detail.status === 'ready') {
|
if (detail.status === 'ready') {
|
||||||
callbacks.onProgress?.({ type: 'batch_done', detail: '抽取完成', done: 100, total: 100, ts: Date.now() });
|
callbacks.onProgress?.({ type: 'batch_done', detail: 'Extraction complete', done: 100, total: 100, ts: Date.now() });
|
||||||
const count = detail.page_count ?? 0;
|
const count = detail.page_count ?? 0;
|
||||||
callbacks.onComplete?.({ total: count, ingested: count });
|
callbacks.onComplete?.({ total: count, ingested: count });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (detail.status === 'failed') {
|
if (detail.status === 'failed') {
|
||||||
callbacks.onError?.(detail.sync_error || '抽取失败');
|
callbacks.onError?.(detail.sync_error || 'Extraction failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callbacks.onError?.('抽取超时,请稍后刷新查看最新状态');
|
callbacks.onError?.('Extraction timed out. Refresh later to see the latest status.');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
callbacks.onError?.(err.message || String(err));
|
callbacks.onError?.(err.message || String(err));
|
||||||
}
|
}
|
||||||
@ -381,7 +381,7 @@ export const knowledgeApi = {
|
|||||||
'/wiki/page/read', { wiki_id: wikiId, refs: [path] }
|
'/wiki/page/read', { wiki_id: wikiId, refs: [path] }
|
||||||
);
|
);
|
||||||
const item = d.items?.[0];
|
const item = d.items?.[0];
|
||||||
if (item?.not_found) throw new Error(`页面不存在: ${path}`);
|
if (item?.not_found) throw new Error(`Page not found: ${path}`);
|
||||||
return { content: item?.content ?? '' };
|
return { content: item?.content ?? '' };
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -498,7 +498,7 @@ export async function pollWikiStatus(wikiId: string, maxAttempts = 30, intervalM
|
|||||||
if (detail.status === 'ready' || detail.status === 'failed') return detail;
|
if (detail.status === 'ready' || detail.status === 'failed') return detail;
|
||||||
await new Promise(r => setTimeout(r, intervalMs));
|
await new Promise(r => setTimeout(r, intervalMs));
|
||||||
}
|
}
|
||||||
throw new Error(`Wiki ${wikiId} ingest 超时`);
|
throw new Error(`Wiki ${wikiId} ingest timed out`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 轮询 code-graph sync 状态 */
|
/** 轮询 code-graph sync 状态 */
|
||||||
@ -508,5 +508,5 @@ export async function pollCodeGraphStatus(codeGraphId: string, maxAttempts = 30,
|
|||||||
if (detail.status === 'ready' || detail.status === 'failed') return detail;
|
if (detail.status === 'ready' || detail.status === 'failed') return detail;
|
||||||
await new Promise(r => setTimeout(r, intervalMs));
|
await new Promise(r => setTimeout(r, intervalMs));
|
||||||
}
|
}
|
||||||
throw new Error(`CodeGraph ${codeGraphId} sync 超时`);
|
throw new Error(`CodeGraph ${codeGraphId} sync timed out`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,7 +117,7 @@ async function skillCall<T>(action: string, body: Record<string, unknown>): Prom
|
|||||||
body: JSON.stringify(stripEmpty(body)),
|
body: JSON.stringify(stripEmpty(body)),
|
||||||
});
|
});
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
throw new SkillApiError(401, 'Unauthorized - 用户登录已失效或缺少用户密钥', '');
|
throw new SkillApiError(401, 'Unauthorized - your session has expired or is missing a user key', '');
|
||||||
}
|
}
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let envelope: SkillEnvelope<T>;
|
let envelope: SkillEnvelope<T>;
|
||||||
|
|||||||
@ -53,8 +53,8 @@ export const tea = {
|
|||||||
return Modal.confirm({
|
return Modal.confirm({
|
||||||
message: opts.message,
|
message: opts.message,
|
||||||
description: opts.description,
|
description: opts.description,
|
||||||
okText: opts.okText ?? '确认',
|
okText: opts.okText ?? 'Confirm',
|
||||||
cancelText: opts.cancelText ?? '取消',
|
cancelText: opts.cancelText ?? 'Cancel',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export const tea = {
|
|||||||
: `request_id: ${input.requestId}`
|
: `request_id: ${input.requestId}`
|
||||||
: input.detail;
|
: input.detail;
|
||||||
notification.error({
|
notification.error({
|
||||||
title: input.title ?? '操作失败',
|
title: input.title ?? 'Operation failed',
|
||||||
description: desc,
|
description: desc,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -99,13 +99,13 @@ export const tea = {
|
|||||||
? `${friendly}\nrequest_id: ${requestId}`
|
? `${friendly}\nrequest_id: ${requestId}`
|
||||||
: friendly;
|
: friendly;
|
||||||
notification.error({
|
notification.error({
|
||||||
title: '操作失败',
|
title: 'Operation failed',
|
||||||
description: desc,
|
description: desc,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
warning: (msg: string) =>
|
warning: (msg: string) =>
|
||||||
notification.warning({
|
notification.warning({
|
||||||
title: '提示',
|
title: 'Notice',
|
||||||
description: msg,
|
description: msg,
|
||||||
}),
|
}),
|
||||||
info: (msg: string) =>
|
info: (msg: string) =>
|
||||||
@ -129,9 +129,9 @@ export const tea = {
|
|||||||
*/
|
*/
|
||||||
confirmDelete: (name: string, detail?: string) =>
|
confirmDelete: (name: string, detail?: string) =>
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
message: `确认删除「${name}」?`,
|
message: `Delete "${name}"?`,
|
||||||
description: detail ?? '删除后不可恢复。',
|
description: detail ?? 'This cannot be undone.',
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,9 +14,9 @@ export function AdminResourceLock() {
|
|||||||
<Card className="_memory-admin-lock-card">
|
<Card className="_memory-admin-lock-card">
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<LockOnIcon size={32} className="_memory-admin-lock-icon" />
|
<LockOnIcon size={32} className="_memory-admin-lock-icon" />
|
||||||
<div className="_memory-admin-lock-title">资源管理功能暂未对管理员开放</div>
|
<div className="_memory-admin-lock-title">Resource management is not yet available to admins</div>
|
||||||
<div className="_memory-admin-lock-desc">
|
<div className="_memory-admin-lock-desc">
|
||||||
Admin 账号当前仅用于组织管理(新建 Team、新增成员)。资源管理请使用普通成员账号操作。
|
Admin accounts are currently for organization management only (creating teams, adding members). Use a regular member account for resource management.
|
||||||
</div>
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export type AllocateAssetType = 'skill' | 'llm_wiki' | 'code_graph' | 'chat_memo
|
|||||||
const TYPE_LABEL: Record<AllocateAssetType, string> = {
|
const TYPE_LABEL: Record<AllocateAssetType, string> = {
|
||||||
skill: 'Skill',
|
skill: 'Skill',
|
||||||
llm_wiki: 'Wiki',
|
llm_wiki: 'Wiki',
|
||||||
code_graph: '代码图谱',
|
code_graph: 'Code graph',
|
||||||
chat_memory: 'Memory'
|
chat_memory: 'Memory'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,14 +49,14 @@ export default function AllocateAssetDialog(props: {
|
|||||||
|
|
||||||
async function submit(): Promise<void> {
|
async function submit(): Promise<void> {
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
setError('请选择 agent。');
|
setError('Please select an agent.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await props.onAllocate(agentId);
|
await props.onAllocate(agentId);
|
||||||
tea.notify.success(`已分配「${props.assetLabel}」→ ${agentId}`);
|
tea.notify.success(`Allocated "${props.assetLabel}" to ${agentId}`);
|
||||||
props.onClose();
|
props.onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tea.notify.error(err);
|
tea.notify.error(err);
|
||||||
@ -66,15 +66,15 @@ export default function AllocateAssetDialog(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption={`分配 ${typeLabel} 到 Agent`} size="s" onClose={props.onClose} disableEscape={submitting}>
|
<Modal visible caption={`Allocate ${typeLabel} to agent`} size="s" onClose={props.onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
{props.team && (
|
{props.team && (
|
||||||
<Form.Item label="所属 Team">
|
<Form.Item label="Team">
|
||||||
<Form.Text>{props.team.name} <Tag size="sm">{props.team.team_id}</Tag></Form.Text>
|
<Form.Text>{props.team.name} <Tag size="sm">{props.team.team_id}</Tag></Form.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
<Form.Item label="资产" extra="挂到所选 agent 的固定资产库(仅记录归属,不复制内容)">
|
<Form.Item label="Asset" extra="Attached to the selected agent's fixed asset library (this only records ownership, it does not copy content)">
|
||||||
<Form.Text>{props.assetLabel}</Form.Text>
|
<Form.Text>{props.assetLabel}</Form.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Agent" required>
|
<Form.Item label="Agent" required>
|
||||||
@ -82,7 +82,7 @@ export default function AllocateAssetDialog(props: {
|
|||||||
size="full"
|
size="full"
|
||||||
value={agentId}
|
value={agentId}
|
||||||
onChange={setAgentId}
|
onChange={setAgentId}
|
||||||
placeholder={props.agents.length === 0 ? '(暂无 agent)' : '请选择 agent'}
|
placeholder={props.agents.length === 0 ? '(No agents yet)' : 'Select an agent'}
|
||||||
options={props.agents.map((a) => ({ value: a.id, text: `${a.id} · ${a.name}` }))}
|
options={props.agents.map((a) => ({ value: a.id, text: `${a.id} · ${a.name}` }))}
|
||||||
disabled={props.agents.length === 0}
|
disabled={props.agents.length === 0}
|
||||||
/>
|
/>
|
||||||
@ -91,8 +91,8 @@ export default function AllocateAssetDialog(props: {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId} loading={submitting}>分配</Button>
|
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId} loading={submitting}>Allocate</Button>
|
||||||
<Button onClick={props.onClose} disabled={submitting}>取消</Button>
|
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export interface AssetScopeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SCOPE_OPTIONS: Array<{ value: AssetConfigScope; label: string }> = [
|
const SCOPE_OPTIONS: Array<{ value: AssetConfigScope; label: string }> = [
|
||||||
{ value: 'team', label: '团队内可配置' },
|
{ value: 'team', label: 'Configurable by team' },
|
||||||
{ value: 'private', label: '仅自己私有' }
|
{ value: 'private', label: 'Private (owner only)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AssetScopeManager({
|
export default function AssetScopeManager({
|
||||||
@ -68,22 +68,22 @@ export default function AssetScopeManager({
|
|||||||
return (
|
return (
|
||||||
<div className="_memory-asset-scope">
|
<div className="_memory-asset-scope">
|
||||||
<Alert type="info">
|
<Alert type="info">
|
||||||
<span className="_memory-asset-scope-alert-title">{label} · 可配置范围</span>
|
<span className="_memory-asset-scope-alert-title">{label} · Configurable scope</span>
|
||||||
<div className="_memory-asset-scope-alert-desc">
|
<div className="_memory-asset-scope-alert-desc">
|
||||||
每个 owner 可以管理自己的 {label}:选择
|
Each owner can manage their own {label}: choose
|
||||||
<span className="_memory-asset-scope-alert-em">
|
<span className="_memory-asset-scope-alert-em">
|
||||||
<UsergroupIcon size={12} /> 团队内可配置
|
<UsergroupIcon size={12} /> Configurable by team
|
||||||
</span>
|
</span>
|
||||||
(团队成员都能改)或
|
(any team member can change it) or
|
||||||
<span className="_memory-asset-scope-alert-em">
|
<span className="_memory-asset-scope-alert-em">
|
||||||
<LockOnIcon size={12} /> 仅自己私有
|
<LockOnIcon size={12} /> Private (owner only)
|
||||||
</span>
|
</span>
|
||||||
(只有你能改)。只有资产 owner 与团队管理员可切换。
|
(only you can change it). Only the asset owner and team admins can switch this.
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className="_memory-asset-scope-empty">当前团队下还没有 {label} 资产。</div>
|
<div className="_memory-asset-scope-empty">No {label} assets in the current team yet.</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="_memory-asset-scope-list">
|
<ul className="_memory-asset-scope-list">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
@ -103,10 +103,10 @@ export default function AssetScopeManager({
|
|||||||
{effectiveOwner ? (
|
{effectiveOwner ? (
|
||||||
<Text theme="weak" className="_memory-asset-scope-item-owner">
|
<Text theme="weak" className="_memory-asset-scope-item-owner">
|
||||||
owner <span className="_memory-asset-scope-item-owner-id">@{effectiveOwner}</span>
|
owner <span className="_memory-asset-scope-item-owner-id">@{effectiveOwner}</span>
|
||||||
{ownerIsMe && <Text theme="primary"> (你)</Text>}
|
{ownerIsMe && <Text theme="primary"> (you)</Text>}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text theme="weak" className="_memory-asset-scope-item-owner">无归属</Text>
|
<Text theme="weak" className="_memory-asset-scope-item-owner">Unowned</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{item.meta && (
|
{item.meta && (
|
||||||
@ -127,7 +127,7 @@ export default function AssetScopeManager({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tag theme={scope === 'private' ? 'default' : 'success'} size="sm">
|
<Tag theme={scope === 'private' ? 'default' : 'success'} size="sm">
|
||||||
{scope === 'private' ? '仅自己私有' : '团队内可配置'}
|
{scope === 'private' ? 'Private (owner only)' : 'Configurable by team'}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -71,8 +71,8 @@ function isValidGitHttpUrl(raw: string): boolean {
|
|||||||
|
|
||||||
type ScopeTab = 'team' | 'fixed';
|
type ScopeTab = 'team' | 'fixed';
|
||||||
const SCOPE_LABELS: Record<ScopeTab, string> = {
|
const SCOPE_LABELS: Record<ScopeTab, string> = {
|
||||||
team: '团队 Code 池',
|
team: 'Team code pool',
|
||||||
fixed: 'Agent 资产',
|
fixed: 'Agent assets',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +84,7 @@ function CodeOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
|
|||||||
return (
|
return (
|
||||||
<span title={`Owner: ${userId}`}>
|
<span title={`Owner: ${userId}`}>
|
||||||
@{name || userId}
|
@{name || userId}
|
||||||
{userId === currentUserId && <span className="_codelist-card-meta-you">(你)</span>}
|
{userId === currentUserId && <span className="_codelist-card-meta-you">(you)</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -92,18 +92,18 @@ function CodeOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
|
|||||||
// 状态 → Tea Tag 语义主题映射(soft 变体),对齐 Memory 的 statusTheme。
|
// 状态 → Tea Tag 语义主题映射(soft 变体),对齐 Memory 的 statusTheme。
|
||||||
function statusLabel(s: string) {
|
function statusLabel(s: string) {
|
||||||
const map: Record<string, [string, 'default' | 'success' | 'warning' | 'error']> = {
|
const map: Record<string, [string, 'default' | 'success' | 'warning' | 'error']> = {
|
||||||
ready: ['就绪', 'success'],
|
ready: ['Ready', 'success'],
|
||||||
pending: ['排队中', 'warning'],
|
pending: ['Queued', 'warning'],
|
||||||
processing: ['构建中', 'warning'],
|
processing: ['Building', 'warning'],
|
||||||
failed: ['失败', 'error'],
|
failed: ['Failed', 'error'],
|
||||||
cloning: ['克隆中', 'warning'],
|
cloning: ['Cloning', 'warning'],
|
||||||
indexing: ['索引中', 'warning'],
|
indexing: ['Indexing', 'warning'],
|
||||||
syncing: ['同步中', 'warning'],
|
syncing: ['Syncing', 'warning'],
|
||||||
error: ['错误', 'error'],
|
error: ['Error', 'error'],
|
||||||
missing: ['已丢失', 'error'],
|
missing: ['Lost', 'error'],
|
||||||
};
|
};
|
||||||
const [label, theme] = map[s] ?? [s, 'default'];
|
const [label, theme] = map[s] ?? [s, 'default'];
|
||||||
const hint = (s === 'pending' || s === 'processing') ? ' · 可能需要数分钟' : '';
|
const hint = (s === 'pending' || s === 'processing') ? ' · may take a few minutes' : '';
|
||||||
return <Tag theme={theme} variant="soft" size="sm">{label}{hint}</Tag>;
|
return <Tag theme={theme} variant="soft" size="sm">{label}{hint}</Tag>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ export default function CodeSourcesPanel() {
|
|||||||
const items = await knowledgeApi.code.agentFixed(agentFilter);
|
const items = await knowledgeApi.code.agentFixed(agentFilter);
|
||||||
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
|
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '加载固定资产失败');
|
tea.notify.error(e?.message || 'Failed to load fixed assets');
|
||||||
setFixedBoundIds(new Set());
|
setFixedBoundIds(new Set());
|
||||||
}
|
}
|
||||||
}, [agentFilter]);
|
}, [agentFilter]);
|
||||||
@ -287,7 +287,7 @@ export default function CodeSourcesPanel() {
|
|||||||
// (callback S2S 是主力,这里只是兜底,但失败要可见)
|
// (callback S2S 是主力,这里只是兜底,但失败要可见)
|
||||||
const msg = e?.message || String(e);
|
const msg = e?.message || String(e);
|
||||||
if (!/already|exist|409|registered|ok/i.test(msg)) {
|
if (!/already|exist|409|registered|ok/i.test(msg)) {
|
||||||
tea.notify.error(`注册 meta 失败: ${msg}`);
|
tea.notify.error(`Failed to register meta: ${msg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toRemove.push(detail.code_graph_id);
|
toRemove.push(detail.code_graph_id);
|
||||||
@ -323,19 +323,19 @@ export default function CodeSourcesPanel() {
|
|||||||
async function handleUnbindCode(codeGraphId: string) {
|
async function handleUnbindCode(codeGraphId: string) {
|
||||||
if (!agentFilter) return;
|
if (!agentFilter) return;
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: '确认解绑该代码图谱?',
|
message: 'Unbind this code graph?',
|
||||||
description: '将从当前 agent 移除该代码图谱绑定。',
|
description: 'This removes the code graph binding from the current agent.',
|
||||||
okText: '解绑',
|
okText: 'Unbind',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.code.unbind(codeGraphId, agentFilter);
|
await knowledgeApi.code.unbind(codeGraphId, agentFilter);
|
||||||
tea.notify.success('已解绑');
|
tea.notify.success('Unbound');
|
||||||
if (selectedCodeAsset?.cgId === codeGraphId) setSelectedCodeAsset(null);
|
if (selectedCodeAsset?.cgId === codeGraphId) setSelectedCodeAsset(null);
|
||||||
await fetchFixedBindings();
|
await fetchFixedBindings();
|
||||||
await fetchSources();
|
await fetchSources();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '解绑失败');
|
tea.notify.error(e?.message || 'Unbind failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +344,7 @@ export default function CodeSourcesPanel() {
|
|||||||
if (!repo || !formBranch.trim() || !activeTeamId) return;
|
if (!repo || !formBranch.trim() || !activeTeamId) return;
|
||||||
// 防御性校验:按钮已按 validUrl 禁用,这里再挡一层防止绕过
|
// 防御性校验:按钮已按 validUrl 禁用,这里再挡一层防止绕过
|
||||||
if (!isValidGitHttpUrl(repo)) {
|
if (!isValidGitHttpUrl(repo)) {
|
||||||
tea.notify.error('请输入合法的 HTTPS Git 仓库地址,且必须以 .git 结尾(如 https://gitlab.example.com/namespace/repo.git),不能含空格。');
|
tea.notify.error('Enter a valid HTTPS git repository URL ending in .git (e.g. https://gitlab.example.com/namespace/repo.git), with no spaces.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -353,7 +353,7 @@ export default function CodeSourcesPanel() {
|
|||||||
setShowRegister(false); setFormRepo(''); setFormBranch('main');
|
setShowRegister(false); setFormRepo(''); setFormBranch('main');
|
||||||
setScopeTab('team');
|
setScopeTab('team');
|
||||||
setInFlight((prev) => [...prev.filter((x) => x.code_graph_id !== detail.code_graph_id), detail]);
|
setInFlight((prev) => [...prev.filter((x) => x.code_graph_id !== detail.code_graph_id), detail]);
|
||||||
tea.notify.info('仓库已注册,正在构建代码图谱,可能需要数分钟');
|
tea.notify.info('Repository registered. Building the code graph — this may take a few minutes.');
|
||||||
fetchSources();
|
fetchSources();
|
||||||
} catch (e: any) { tea.notify.error(e); }
|
} catch (e: any) { tea.notify.error(e); }
|
||||||
finally { setSubmitting(false); }
|
finally { setSubmitting(false); }
|
||||||
@ -368,8 +368,8 @@ export default function CodeSourcesPanel() {
|
|||||||
const source = sources.find(s => s.code_graph_id === cgId);
|
const source = sources.find(s => s.code_graph_id === cgId);
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确定要删除仓库「${source.repo_name || source.repo_url} (${source.branch})」吗?`,
|
message: `Delete repository "${source.repo_name || source.repo_url} (${source.branch})"?`,
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
@ -381,7 +381,7 @@ export default function CodeSourcesPanel() {
|
|||||||
setInFlight((prev) => prev.filter((x) => x.code_graph_id !== cgId));
|
setInFlight((prev) => prev.filter((x) => x.code_graph_id !== cgId));
|
||||||
if (selectedCodeAsset?.cgId === cgId) setSelectedCodeAsset(null);
|
if (selectedCodeAsset?.cgId === cgId) setSelectedCodeAsset(null);
|
||||||
if (selectedCgId === cgId) setSubView('list');
|
if (selectedCgId === cgId) setSubView('list');
|
||||||
tea.notify.success('已删除');
|
tea.notify.success('Deleted');
|
||||||
fetchSources();
|
fetchSources();
|
||||||
} catch (e: any) { tea.notify.error(e); }
|
} catch (e: any) { tea.notify.error(e); }
|
||||||
};
|
};
|
||||||
@ -447,14 +447,14 @@ export default function CodeSourcesPanel() {
|
|||||||
<div className="_codedetail-header-left">
|
<div className="_codedetail-header-left">
|
||||||
<CodeIcon size={18} />
|
<CodeIcon size={18} />
|
||||||
<span className="_codedetail-title" title={selRepo}>{selRepo}</span>
|
<span className="_codedetail-title" title={selRepo}>{selRepo}</span>
|
||||||
<Text theme="label">分支 {selBranch}</Text>
|
<Text theme="label">Branch {selBranch}</Text>
|
||||||
{selected?.commit_hash && <Text theme="label" className="_codedetail-mono">@ {selected.commit_hash}</Text>}
|
{selected?.commit_hash && <Text theme="label" className="_codedetail-mono">@ {selected.commit_hash}</Text>}
|
||||||
{selected && statusLabel(selected.status)}
|
{selected && statusLabel(selected.status)}
|
||||||
{selected?.last_sync_at && <Text theme="label">{new Date(selected.last_sync_at).toLocaleString()}</Text>}
|
{selected?.last_sync_at && <Text theme="label">{new Date(selected.last_sync_at).toLocaleString()}</Text>}
|
||||||
</div>
|
</div>
|
||||||
<div className="_codedetail-header-actions">
|
<div className="_codedetail-header-actions">
|
||||||
<Button type="primary" onClick={() => handleSync(selectedCgId)}>
|
<Button type="primary" onClick={() => handleSync(selectedCgId)}>
|
||||||
<span className="_codedetail-inline-icon"><RefreshIcon size={14} />同步</span>
|
<span className="_codedetail-inline-icon"><RefreshIcon size={14} />Sync</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -466,22 +466,22 @@ export default function CodeSourcesPanel() {
|
|||||||
{/* 统计 */}
|
{/* 统计 */}
|
||||||
{selected?.stats && (
|
{selected?.stats && (
|
||||||
<div className="_codedetail-stats">
|
<div className="_codedetail-stats">
|
||||||
<MetricsBoard title="文件" value={selected.stats.files?.toLocaleString() ?? '-'} />
|
<MetricsBoard title="Files" value={selected.stats.files?.toLocaleString() ?? '-'} />
|
||||||
<MetricsBoard title="图节点" value={selected.stats.nodes?.toLocaleString() ?? '-'} />
|
<MetricsBoard title="Graph nodes" value={selected.stats.nodes?.toLocaleString() ?? '-'} />
|
||||||
<MetricsBoard title="图边" value={selected.stats.edges?.toLocaleString() ?? '-'} />
|
<MetricsBoard title="Graph edges" value={selected.stats.edges?.toLocaleString() ?? '-'} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 仓库信息 */}
|
{/* 仓库信息 */}
|
||||||
{selected && (
|
{selected && (
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Body title="仓库信息">
|
<Card.Body title="Repository info">
|
||||||
<div className="_codedetail-info-grid">
|
<div className="_codedetail-info-grid">
|
||||||
<Text theme="label">Code Graph ID</Text>
|
<Text theme="label">Code Graph ID</Text>
|
||||||
<Text className="_codedetail-mono">{selected.code_graph_id}</Text>
|
<Text className="_codedetail-mono">{selected.code_graph_id}</Text>
|
||||||
<Text theme="label">Git URL</Text>
|
<Text theme="label">Git URL</Text>
|
||||||
<Text className="_codedetail-mono">{selected.repo_url || '—'}</Text>
|
<Text className="_codedetail-mono">{selected.repo_url || '—'}</Text>
|
||||||
<Text theme="label">最后同步</Text>
|
<Text theme="label">Last synced</Text>
|
||||||
<Text>{selected.last_sync_at ? new Date(selected.last_sync_at).toLocaleString() : '—'}</Text>
|
<Text>{selected.last_sync_at ? new Date(selected.last_sync_at).toLocaleString() : '—'}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
@ -490,9 +490,9 @@ export default function CodeSourcesPanel() {
|
|||||||
|
|
||||||
{/* 代码搜索 */}
|
{/* 代码搜索 */}
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Body title="代码搜索">
|
<Card.Body title="Code search">
|
||||||
<Text theme="label" parent="div" className="_codedetail-hint">
|
<Text theme="label" parent="div" className="_codedetail-hint">
|
||||||
按符号名快速定位,只返回匹配的函数 / 类 / 变量所在的文件与行号,不含代码原文。适合"这个符号在哪里"。
|
Look up by symbol name — returns only the file and line number of matching functions/classes/variables, not the source itself. Good for "where is this symbol".
|
||||||
</Text>
|
</Text>
|
||||||
<div className="_codedetail-search-row">
|
<div className="_codedetail-search-row">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@ -500,7 +500,7 @@ export default function CodeSourcesPanel() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(v) => setSearchQuery(v)}
|
onChange={(v) => setSearchQuery(v)}
|
||||||
onSearch={() => void handleSearch()}
|
onSearch={() => void handleSearch()}
|
||||||
placeholder="输入符号名(函数 / 类 / 变量),返回其所在位置…"
|
placeholder="Enter a symbol name (function/class/variable) to find its location..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{searching && <StatusTip status="loading" />}
|
{searching && <StatusTip status="loading" />}
|
||||||
@ -514,9 +514,9 @@ export default function CodeSourcesPanel() {
|
|||||||
|
|
||||||
{/* 代码探索 */}
|
{/* 代码探索 */}
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Body title="代码探索">
|
<Card.Body title="Code exploration">
|
||||||
<Text theme="label" parent="div" className="_codedetail-hint">
|
<Text theme="label" parent="div" className="_codedetail-hint">
|
||||||
一次返回相关文件的完整原文与调用关系,让 AI 直接拿到上下文,无需再逐个 grep / 读文件。适合"这个功能是怎么实现的"。
|
Returns the full source of related files along with call relationships in one go, giving the AI direct context without grepping or reading files one by one. Good for "how this feature is implemented".
|
||||||
</Text>
|
</Text>
|
||||||
<div className="_codedetail-search-row">
|
<div className="_codedetail-search-row">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@ -524,7 +524,7 @@ export default function CodeSourcesPanel() {
|
|||||||
value={exploreQuery}
|
value={exploreQuery}
|
||||||
onChange={(v) => setExploreQuery(v)}
|
onChange={(v) => setExploreQuery(v)}
|
||||||
onSearch={() => void handleExplore()}
|
onSearch={() => void handleExplore()}
|
||||||
placeholder="用自然语言或符号名描述要理解的功能 / 流程,返回相关文件原文…"
|
placeholder="Describe the feature or flow you want to understand, in natural language or a symbol name..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{exploring && <StatusTip status="loading" />}
|
{exploring && <StatusTip status="loading" />}
|
||||||
@ -558,18 +558,18 @@ export default function CodeSourcesPanel() {
|
|||||||
value={agentFilter}
|
value={agentFilter}
|
||||||
onChange={setAgentFilter}
|
onChange={setAgentFilter}
|
||||||
disabled={teamAgents.length === 0}
|
disabled={teamAgents.length === 0}
|
||||||
placeholder="无可选 Agent"
|
placeholder="No agent available"
|
||||||
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name}(${agent.id})` }))}
|
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name} (${agent.id})` }))}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
subtitle={activeTeam ? `${activeTeam.name} · 共 ${stats.total} 个仓库` : `共 ${stats.total} 个仓库`}
|
subtitle={activeTeam ? `${activeTeam.name} · ${stats.total} repositories total` : `${stats.total} repositories total`}
|
||||||
actions={scopeTab !== 'fixed' ? (
|
actions={scopeTab !== 'fixed' ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setAllocateTarget(selectedCodeAsset)}
|
onClick={() => setAllocateTarget(selectedCodeAsset)}
|
||||||
disabled={!selectedCodeAsset}
|
disabled={!selectedCodeAsset}
|
||||||
tooltip={!selectedCodeAsset ? '请先选中一条代码资产' : undefined}
|
tooltip={!selectedCodeAsset ? 'Select a code asset first' : undefined}
|
||||||
>
|
>
|
||||||
分配到 Agent
|
Assign to agent
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
/>
|
/>
|
||||||
@ -577,29 +577,29 @@ export default function CodeSourcesPanel() {
|
|||||||
<Card className="_asset-code-content-card">
|
<Card className="_asset-code-content-card">
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<div className="_asset-code-stats">
|
<div className="_asset-code-stats">
|
||||||
<MetricsBoard title="仓库总数" value={stats.total} />
|
<MetricsBoard title="Total repositories" value={stats.total} />
|
||||||
<MetricsBoard title="已就绪" value={stats.ready} />
|
<MetricsBoard title="Ready" value={stats.ready} />
|
||||||
<MetricsBoard title="处理中" value={stats.processing} />
|
<MetricsBoard title="Processing" value={stats.processing} />
|
||||||
<MetricsBoard title="文件总数" value={stats.totalFiles} />
|
<MetricsBoard title="Total files" value={stats.totalFiles} />
|
||||||
</div>
|
</div>
|
||||||
<Table.ActionPanel>
|
<Table.ActionPanel>
|
||||||
<Justify
|
<Justify
|
||||||
left={<Button type="primary" onClick={() => setShowRegister(true)}>+ 注册仓库</Button>}
|
left={<Button type="primary" onClick={() => setShowRegister(true)}>+ Register repository</Button>}
|
||||||
right={(
|
right={(
|
||||||
<div className="_asset-code-toolbar">
|
<div className="_asset-code-toolbar">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={setKeyword}
|
onChange={setKeyword}
|
||||||
placeholder="搜索名称 / 分支 / ID"
|
placeholder="Search name / branch / ID"
|
||||||
/>
|
/>
|
||||||
<Segment
|
<Segment
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', text: '全部状态' },
|
{ value: 'all', text: 'All statuses' },
|
||||||
{ value: 'ready', text: '就绪' },
|
{ value: 'ready', text: 'Ready' },
|
||||||
{ value: 'processing', text: '处理中' },
|
{ value: 'processing', text: 'Processing' },
|
||||||
{ value: 'error', text: '异常' },
|
{ value: 'error', text: 'Error' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Segment
|
<Segment
|
||||||
@ -623,13 +623,13 @@ export default function CodeSourcesPanel() {
|
|||||||
emptyText={(
|
emptyText={(
|
||||||
<div className="_asset-code-empty">
|
<div className="_asset-code-empty">
|
||||||
<CodeIcon size="large" />
|
<CodeIcon size="large" />
|
||||||
<Text>暂无已注册仓库</Text>
|
<Text>No registered repositories</Text>
|
||||||
<Text theme="label">点击上方“+ 注册仓库”注册第一个</Text>
|
<Text theme="label">Click "+ Register repository" above to register your first one</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : filteredSources.length === 0 ? (
|
) : filteredSources.length === 0 ? (
|
||||||
<StatusTip status="empty" emptyText="没有匹配的仓库,试试调整搜索或筛选条件。" />
|
<StatusTip status="empty" emptyText="No matching repositories. Try adjusting your search or filters." />
|
||||||
) : viewMode === 'card' ? (
|
) : viewMode === 'card' ? (
|
||||||
<div className="_codelist-grid">
|
<div className="_codelist-grid">
|
||||||
{filteredSources.map((source) => {
|
{filteredSources.map((source) => {
|
||||||
@ -645,7 +645,7 @@ export default function CodeSourcesPanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="_codelist-card-head _codelist-card-name-trigger"
|
className="_codelist-card-head _codelist-card-name-trigger"
|
||||||
onClick={(event) => { event.stopPropagation(); openDetail(source.code_graph_id); }}
|
onClick={(event) => { event.stopPropagation(); openDetail(source.code_graph_id); }}
|
||||||
title={`查看 ${repoLabel} 详情`}
|
title={`View ${repoLabel} details`}
|
||||||
>
|
>
|
||||||
<CodeIcon size={16} />
|
<CodeIcon size={16} />
|
||||||
<span className="_codelist-card-name">{repoLabel}</span>
|
<span className="_codelist-card-name">{repoLabel}</span>
|
||||||
@ -653,7 +653,7 @@ export default function CodeSourcesPanel() {
|
|||||||
</button>
|
</button>
|
||||||
<div className="_codelist-card-meta">
|
<div className="_codelist-card-meta">
|
||||||
{statusLabel(source.status)}
|
{statusLabel(source.status)}
|
||||||
<span>分支 {source.branch}</span>
|
<span>Branch {source.branch}</span>
|
||||||
{source.commit_hash && <span className="_codedetail-mono">@ {source.commit_hash}</span>}
|
{source.commit_hash && <span className="_codedetail-mono">@ {source.commit_hash}</span>}
|
||||||
{source.stats && <span>{source.stats.nodes.toLocaleString()} nodes · {source.stats.files.toLocaleString()} files</span>}
|
{source.stats && <span>{source.stats.nodes.toLocaleString()} nodes · {source.stats.files.toLocaleString()} files</span>}
|
||||||
<span>{formatShortTime(source.last_sync_at)}</span>
|
<span>{formatShortTime(source.last_sync_at)}</span>
|
||||||
@ -661,26 +661,26 @@ export default function CodeSourcesPanel() {
|
|||||||
<div className="_codelist-card-owner">
|
<div className="_codelist-card-owner">
|
||||||
<UsergroupIcon size={12} />
|
<UsergroupIcon size={12} />
|
||||||
{scopeTab === 'fixed' ? (
|
{scopeTab === 'fixed' ? (
|
||||||
`固定资产 · ${agentFilter || '未选择 Agent'}`
|
`Fixed asset · ${agentFilter || 'No agent selected'}`
|
||||||
) : source.owner_user_id ? (
|
) : source.owner_user_id ? (
|
||||||
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
||||||
) : (
|
) : (
|
||||||
'团队 Code 池'
|
'Team code pool'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="_codelist-card-id">ID:{source.code_graph_id}</div>
|
<div className="_codelist-card-id">ID: {source.code_graph_id}</div>
|
||||||
<div className="_codelist-card-actions" onClick={(event) => event.stopPropagation()}>
|
<div className="_codelist-card-actions" onClick={(event) => event.stopPropagation()}>
|
||||||
{scopeTab === 'fixed' ? (
|
{scopeTab === 'fixed' ? (
|
||||||
<Button type="weak" onClick={() => handleUnbindCode(source.code_graph_id)}>
|
<Button type="weak" onClick={() => handleUnbindCode(source.code_graph_id)}>
|
||||||
<span className="_codelist-inline-icon"><UsergroupIcon size={14} />解绑</span>
|
<span className="_codelist-inline-icon"><UsergroupIcon size={14} />Unbind</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="weak" onClick={() => setAllocateTarget({ cgId: source.code_graph_id, repo: repoLabel, branch: source.branch })}>分配</Button>
|
<Button type="weak" onClick={() => setAllocateTarget({ cgId: source.code_graph_id, repo: repoLabel, branch: source.branch })}>Assign</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="icon" tooltip="同步" onClick={() => handleSync(source.code_graph_id)}>
|
<Button type="icon" tooltip="Sync" onClick={() => handleSync(source.code_graph_id)}>
|
||||||
<RefreshIcon size={14} />
|
<RefreshIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="icon" tooltip="删除" onClick={() => handleDelete(source.code_graph_id)}>
|
<Button type="icon" tooltip="Delete" onClick={() => handleDelete(source.code_graph_id)}>
|
||||||
<DeleteIcon size={14} />
|
<DeleteIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -696,14 +696,14 @@ export default function CodeSourcesPanel() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
key: 'repo_name',
|
key: 'repo_name',
|
||||||
header: '仓库',
|
header: 'Repository',
|
||||||
width: 250,
|
width: 250,
|
||||||
render: (source) => (
|
render: (source) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="_codelist-row-name"
|
className="_codelist-row-name"
|
||||||
onClick={() => openDetail(source.code_graph_id)}
|
onClick={() => openDetail(source.code_graph_id)}
|
||||||
title={`查看 ${source.repo_name || source.repo_url} 详情`}
|
title={`View ${source.repo_name || source.repo_url} details`}
|
||||||
>
|
>
|
||||||
<CodeIcon size={14} />
|
<CodeIcon size={14} />
|
||||||
<span>{source.repo_name || source.repo_url}</span>
|
<span>{source.repo_name || source.repo_url}</span>
|
||||||
@ -713,13 +713,13 @@ export default function CodeSourcesPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
header: '状态',
|
header: 'Status',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (source) => statusLabel(source.status),
|
render: (source) => statusLabel(source.status),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'branch',
|
key: 'branch',
|
||||||
header: '分支 / Commit',
|
header: 'Branch / commit',
|
||||||
width: 190,
|
width: 190,
|
||||||
render: (source) => (
|
render: (source) => (
|
||||||
<span className="_codelist-branch">
|
<span className="_codelist-branch">
|
||||||
@ -730,25 +730,25 @@ export default function CodeSourcesPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'stats',
|
key: 'stats',
|
||||||
header: '图谱统计',
|
header: 'Graph stats',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (source) => source.stats ? `${source.stats.nodes.toLocaleString()} nodes · ${source.stats.files.toLocaleString()} files` : '—',
|
render: (source) => source.stats ? `${source.stats.nodes.toLocaleString()} nodes · ${source.stats.files.toLocaleString()} files` : '—',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'owner',
|
key: 'owner',
|
||||||
header: '归属',
|
header: 'Owner',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (source) => scopeTab === 'fixed' ? (
|
render: (source) => scopeTab === 'fixed' ? (
|
||||||
<span className="_codelist-inline-icon"><UsergroupIcon size={12} />{agentFilter || '未选择 Agent'}</span>
|
<span className="_codelist-inline-icon"><UsergroupIcon size={12} />{agentFilter || 'No agent selected'}</span>
|
||||||
) : source.owner_user_id ? (
|
) : source.owner_user_id ? (
|
||||||
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
||||||
) : (
|
) : (
|
||||||
<Text theme="label">团队池</Text>
|
<Text theme="label">Team pool</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'last_sync_at',
|
key: 'last_sync_at',
|
||||||
header: '最后更新时间',
|
header: 'Last updated',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (source) => <Text theme="label">{formatShortTime(source.last_sync_at)}</Text>,
|
render: (source) => <Text theme="label">{formatShortTime(source.last_sync_at)}</Text>,
|
||||||
},
|
},
|
||||||
@ -760,7 +760,7 @@ export default function CodeSourcesPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '操作',
|
header: 'Actions',
|
||||||
width: 280,
|
width: 280,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (source) => {
|
render: (source) => {
|
||||||
@ -768,12 +768,12 @@ export default function CodeSourcesPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="_codelist-table-actions">
|
<div className="_codelist-table-actions">
|
||||||
{scopeTab === 'fixed' ? (
|
{scopeTab === 'fixed' ? (
|
||||||
<Button type="link" onClick={() => handleUnbindCode(source.code_graph_id)}>解绑</Button>
|
<Button type="link" onClick={() => handleUnbindCode(source.code_graph_id)}>Unbind</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="link" onClick={() => setAllocateTarget({ cgId: source.code_graph_id, repo: repoLabel, branch: source.branch })}>分配</Button>
|
<Button type="link" onClick={() => setAllocateTarget({ cgId: source.code_graph_id, repo: repoLabel, branch: source.branch })}>Assign</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="link" onClick={() => handleSync(source.code_graph_id)}>同步</Button>
|
<Button type="link" onClick={() => handleSync(source.code_graph_id)}>Sync</Button>
|
||||||
<Button type="link" onClick={() => handleDelete(source.code_graph_id)} className="_codelist-delete-action">删除</Button>
|
<Button type="link" onClick={() => handleDelete(source.code_graph_id)} className="_codelist-delete-action">Delete</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -792,10 +792,10 @@ export default function CodeSourcesPanel() {
|
|||||||
// 已输入内容、非 SSH、但又不是合法 http(s) 地址 → 提示格式错误。
|
// 已输入内容、非 SSH、但又不是合法 http(s) 地址 → 提示格式错误。
|
||||||
const showUrlError = !!trimmedRepo && !isSsh && !validUrl;
|
const showUrlError = !!trimmedRepo && !isSsh && !validUrl;
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="注册代码仓库" size="m" onClose={() => setShowRegister(false)} disableEscape={submitting}>
|
<Modal visible caption="Register code repository" size="m" onClose={() => setShowRegister(false)} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="Git URL" required extra="注册后将自动 clone 并建立代码索引。">
|
<Form.Item label="Git URL" required extra="Automatically clones and builds a code index after registration.">
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={formRepo}
|
value={formRepo}
|
||||||
@ -804,21 +804,21 @@ export default function CodeSourcesPanel() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{isSsh && (
|
{isSsh && (
|
||||||
<Form.Item><Alert type="warning">当前版本不支持 SSH 格式的仓库地址,请改用 HTTPS 格式(如 https://gitlab.example.com/namespace/repo.git)。</Alert></Form.Item>
|
<Form.Item><Alert type="warning">The current version doesn't support SSH-style repository URLs. Use HTTPS instead (e.g. https://gitlab.example.com/namespace/repo.git).</Alert></Form.Item>
|
||||||
)}
|
)}
|
||||||
{showUrlError && (
|
{showUrlError && (
|
||||||
<Form.Item><Alert type="error">请输入合法的 HTTP(S) Git 仓库地址,且必须以 .git 结尾(如 https://gitlab.example.com/namespace/repo.git),不能含空格。</Alert></Form.Item>
|
<Form.Item><Alert type="error">Enter a valid HTTP(S) git repository URL ending in .git (e.g. https://gitlab.example.com/namespace/repo.git), with no spaces.</Alert></Form.Item>
|
||||||
)}
|
)}
|
||||||
<Form.Item label="分支" required>
|
<Form.Item label="Branch" required>
|
||||||
<Input size="full" value={formBranch} onChange={setFormBranch} placeholder="main" />
|
<Input size="full" value={formBranch} onChange={setFormBranch} placeholder="main" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={handleRegister} disabled={submitting || !formBranch.trim() || !validUrl} loading={submitting}>
|
<Button type="primary" onClick={handleRegister} disabled={submitting || !formBranch.trim() || !validUrl} loading={submitting}>
|
||||||
{submitting ? '注册中…' : '注册'}
|
{submitting ? 'Registering…' : 'Register'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setShowRegister(false)} disabled={submitting}>取消</Button>
|
<Button onClick={() => setShowRegister(false)} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -833,9 +833,9 @@ export default function CodeSourcesPanel() {
|
|||||||
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
|
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
|
||||||
onClose={() => setAllocateTarget(null)}
|
onClose={() => setAllocateTarget(null)}
|
||||||
onAllocate={async (agentId) => {
|
onAllocate={async (agentId) => {
|
||||||
if (!activeTeamId) throw new Error('请先选择 team');
|
if (!activeTeamId) throw new Error('Select a team first');
|
||||||
await knowledgeApi.code.allocate(activeTeamId, allocateTarget.cgId, agentId);
|
await knowledgeApi.code.allocate(activeTeamId, allocateTarget.cgId, agentId);
|
||||||
tea.notify.success('已分配到 Agent');
|
tea.notify.success('Assigned to agent');
|
||||||
await fetchSources();
|
await fetchSources();
|
||||||
if (scopeTab === 'fixed') await fetchFixedBindings();
|
if (scopeTab === 'fixed') await fetchFixedBindings();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -48,37 +48,37 @@ export function AllocateMemoryDialog({
|
|||||||
// 文案分支:不同来源说不同的话,避免"团队池里"这种错误措辞出现在 personal tab。
|
// 文案分支:不同来源说不同的话,避免"团队池里"这种错误措辞出现在 personal tab。
|
||||||
const description = memorySource === 'team' ? (
|
const description = memorySource === 'team' ? (
|
||||||
<>
|
<>
|
||||||
把团队池里的记忆块 <Text theme="strong" parent="span">{memoryTitle}</Text> 绑定到所选 agent 的固定资产。
|
Bind the memory block <Text theme="strong" parent="span">{memoryTitle}</Text> from the team pool to the selected agent's fixed assets.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
把记忆块 <Text theme="strong" parent="span">{memoryTitle}</Text> 分配到所选 agent 的固定资产。
|
Allocate the memory block <Text theme="strong" parent="span">{memoryTitle}</Text> to the selected agent's fixed assets.
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="分配记忆块到 Agent" size="s" onClose={onClose} disableEscape={submitting}>
|
<Modal visible caption="Allocate memory block to agent" size="s" onClose={onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="说明"><Form.Text>{description}</Form.Text></Form.Item>
|
<Form.Item label="Description"><Form.Text>{description}</Form.Text></Form.Item>
|
||||||
{agents.length === 0 ? (
|
{agents.length === 0 ? (
|
||||||
<Alert type="warning">
|
<Alert type="warning">
|
||||||
没有可分配的 Agent。可能的原因:
|
No agents available to allocate to. Possible reasons:
|
||||||
<br />· 你还没有创建任何 Agent —— 请先到「Agent 管理」创建一个。
|
<br />· You haven't created any agents yet — create one in "Agent management" first.
|
||||||
<br />· 选中的记忆块是你某个 Agent 的自有记忆,不能分配给同一个 Agent(规则:不能把 Agent 自己的记忆再分配给自己)。
|
<br />· The selected memory block is an agent's own memory and can't be allocated to that same agent (an agent's own memory can't be allocated back to itself).
|
||||||
<br />· 该记忆块已绑定到你所有的 Agent,无需重复分配。
|
<br />· This memory block is already bound to all of your agents, so no further allocation is needed.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item label="Agent" required>
|
<Form.Item label="Agent" required>
|
||||||
<Select size="full" value={agentId} onChange={setAgentId} placeholder="无可选 agent"
|
<Select size="full" value={agentId} onChange={setAgentId} placeholder="No agent available"
|
||||||
options={agents.map((a) => ({ value: a.agent_id, text: a.name }))} />
|
options={agents.map((a) => ({ value: a.agent_id, text: a.name }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void submit()} disabled={!agentId || submitting} loading={submitting}>分配</Button>
|
<Button type="primary" onClick={() => void submit()} disabled={!agentId || submitting} loading={submitting}>Allocate</Button>
|
||||||
<Button onClick={onClose} disabled={submitting}>取消</Button>
|
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -72,20 +72,20 @@ export function BlockDetail({
|
|||||||
<div className="text-[11px] text-muted-foreground mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div className="text-[11px] text-muted-foreground mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
{block.agent_id ? (
|
{block.agent_id ? (
|
||||||
<span className="px-1.5 py-0.5 rounded border font-mono text-[10px] inline-flex items-center gap-0.5" style={LAYER_TONE_STYLE.success}>
|
<span className="px-1.5 py-0.5 rounded border font-mono text-[10px] inline-flex items-center gap-0.5" style={LAYER_TONE_STYLE.success}>
|
||||||
<AppIcon size={12} /> 固定到 {agentLabel(block.agent_id)}
|
<AppIcon size={12} /> Fixed to {agentLabel(block.agent_id)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="px-1.5 py-0.5 rounded border text-[10px] inline-flex items-center gap-0.5" style={LAYER_TONE_STYLE.warning}>
|
<span className="px-1.5 py-0.5 rounded border text-[10px] inline-flex items-center gap-0.5" style={LAYER_TONE_STYLE.warning}>
|
||||||
<UsergroupIcon size={12} /> 团队记忆池
|
<UsergroupIcon size={12} /> Team memory pool
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{block.uploaded_by_user_id && (
|
{block.uploaded_by_user_id && (
|
||||||
<>
|
<>
|
||||||
<span>上传:<span className="font-mono">@{block.uploaded_by_user_id}</span></span>
|
<span>Uploaded by: <span className="font-mono">@{block.uploaded_by_user_id}</span></span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span>更新:{new Date(block.updated_at_ms).toLocaleString()}</span>
|
<span>Updated: {new Date(block.updated_at_ms).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -114,7 +114,7 @@ export function BlockDetail({
|
|||||||
<span className={`text-[12px] font-semibold ${active ? '' : 'text-foreground/70'}`}>{l.label}</span>
|
<span className={`text-[12px] font-semibold ${active ? '' : 'text-foreground/70'}`}>{l.label}</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[11px] font-mono ${active ? '' : 'text-muted-foreground'}`}
|
className={`text-[11px] font-mono ${active ? '' : 'text-muted-foreground'}`}
|
||||||
title={known ? undefined : '点击加载该层内容'}
|
title={known ? undefined : 'Click to load this layer\'s content'}
|
||||||
>
|
>
|
||||||
{known ? cnt : '·'}
|
{known ? cnt : '·'}
|
||||||
</span>
|
</span>
|
||||||
@ -175,7 +175,7 @@ export function BlockDetail({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-[12px] px-2 py-6 text-center" style={{ color: 'var(--tea-color-text-tertiary)' }}>该记忆块未保留 L0 对话原文。</div>
|
<div className="text-[12px] px-2 py-6 text-center" style={{ color: 'var(--tea-color-text-tertiary)' }}>This memory block does not retain the L0 raw conversation.</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<AtomicList
|
<AtomicList
|
||||||
@ -188,7 +188,7 @@ export function BlockDetail({
|
|||||||
{showPager && (
|
{showPager && (
|
||||||
<div className="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3 text-[11px] text-muted-foreground">
|
<div className="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3 text-[11px] text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
第 {safePage + 1} / {pageCount} 页 · 当前 {block.layers[layer].length} 条 / 共 {total} 条
|
Page {safePage + 1} / {pageCount} · showing {block.layers[layer].length} of {total} items
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
@ -196,14 +196,14 @@ export function BlockDetail({
|
|||||||
disabled={layerLoading || safePage <= 0}
|
disabled={layerLoading || safePage <= 0}
|
||||||
onClick={() => onLayerPageChange(safePage - 1)}
|
onClick={() => onLayerPageChange(safePage - 1)}
|
||||||
>
|
>
|
||||||
上一页
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="rounded border px-2 py-1 disabled:cursor-not-allowed disabled:opacity-40"
|
className="rounded border px-2 py-1 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
disabled={layerLoading || safePage >= pageCount - 1}
|
disabled={layerLoading || safePage >= pageCount - 1}
|
||||||
onClick={() => onLayerPageChange(safePage + 1)}
|
onClick={() => onLayerPageChange(safePage + 1)}
|
||||||
>
|
>
|
||||||
下一页
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -228,7 +228,7 @@ function AtomicList({
|
|||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-[12px] text-muted-foreground px-2 py-4">
|
<div className="text-[12px] text-muted-foreground px-2 py-4">
|
||||||
该记忆块在 {meta.short} 层暂无条目。可由 curator / 高层提炼后写入。
|
This memory block has no entries at the {meta.short} layer yet. A curator or higher-layer distillation can populate it.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -262,7 +262,7 @@ function AtomicList({
|
|||||||
onClick={() => onLoadItem?.(it.id)}
|
onClick={() => onLoadItem?.(it.id)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? '加载中…' : hasBody ? '收起原文' : '展开原文'}
|
{loading ? 'Loading...' : hasBody ? 'Collapse raw text' : 'Expand raw text'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -274,7 +274,7 @@ function AtomicList({
|
|||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{it.body}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{it.body}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : isL2 ? null : (
|
) : isL2 ? null : (
|
||||||
<div className="mt-1 text-[12px] text-muted-foreground">暂无原文。</div>
|
<div className="mt-1 text-[12px] text-muted-foreground">No raw text yet.</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<pre className="mt-1 text-[12px] text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">{it.body}</pre>
|
<pre className="mt-1 text-[12px] text-foreground/70 whitespace-pre-wrap font-sans leading-relaxed">{it.body}</pre>
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export default function ChatMemoryPanel(
|
|||||||
setBlocks(mapped);
|
setBlocks(mapped);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (seq !== fetchSeqRef.current) return;
|
if (seq !== fetchSeqRef.current) return;
|
||||||
tea.notify.error(e?.message || '加载记忆块失败');
|
tea.notify.error(e?.message || 'Failed to load memory blocks');
|
||||||
setBlocks([]);
|
setBlocks([]);
|
||||||
} finally {
|
} finally {
|
||||||
if (seq === fetchSeqRef.current) setBlocksLoading(false);
|
if (seq === fetchSeqRef.current) setBlocksLoading(false);
|
||||||
@ -253,7 +253,7 @@ export default function ChatMemoryPanel(
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((e: any) => {
|
.catch((e: any) => {
|
||||||
if (!cancelled) tea.notify.error(e?.message || '加载层数据失败');
|
if (!cancelled) tea.notify.error(e?.message || 'Failed to load layer data');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLayerLoading(false);
|
if (!cancelled) setLayerLoading(false);
|
||||||
@ -314,7 +314,7 @@ export default function ChatMemoryPanel(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '加载 L2 原文失败');
|
tea.notify.error(e?.message || 'Failed to load L2 raw text');
|
||||||
} finally {
|
} finally {
|
||||||
setLayerItemLoadingId(null);
|
setLayerItemLoadingId(null);
|
||||||
}
|
}
|
||||||
@ -371,9 +371,9 @@ export default function ChatMemoryPanel(
|
|||||||
// ── 操作 ──
|
// ── 操作 ──
|
||||||
async function handleDeleteBlock(id: string) {
|
async function handleDeleteBlock(id: string) {
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: '确认解绑该记忆块?',
|
message: 'Unbind this memory block?',
|
||||||
description: '将从当前 agent 移除该记忆块绑定。',
|
description: 'This removes the memory block binding from the current agent.',
|
||||||
okText: '解绑',
|
okText: 'Unbind',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
@ -382,9 +382,9 @@ export default function ChatMemoryPanel(
|
|||||||
await chatMemoryApi.unbind(activeTeamId, id, block.agent_id);
|
await chatMemoryApi.unbind(activeTeamId, id, block.agent_id);
|
||||||
setBlocks((prev) => prev.filter((b) => b.id !== id));
|
setBlocks((prev) => prev.filter((b) => b.id !== id));
|
||||||
if (selectedId === id) setSelectedId(null);
|
if (selectedId === id) setSelectedId(null);
|
||||||
tea.notify.success('已解绑');
|
tea.notify.success('Unbound');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '解绑失败');
|
tea.notify.error(e?.message || 'Failed to unbind');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,15 +397,15 @@ export default function ChatMemoryPanel(
|
|||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
if (!activeTeamId || !agent_id) {
|
if (!activeTeamId || !agent_id) {
|
||||||
tea.notify.warning('请先选择一个 Agent');
|
tea.notify.warning('Select an agent first');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await chatMemoryApi.import(activeTeamId, agent_id, messages);
|
await chatMemoryApi.import(activeTeamId, agent_id, messages);
|
||||||
tea.notify.success(`导入成功 · ${messages.length} 条消息,tdai 后台正在蒸馏 L1/L2/L3`);
|
tea.notify.success(`Imported successfully · ${messages.length} messages — the GODCALL backend is distilling L1/L2/L3`);
|
||||||
setShowImport(false);
|
setShowImport(false);
|
||||||
fetchBlocks();
|
fetchBlocks();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '导入失败');
|
tea.notify.error(e?.message || 'Import failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,18 +415,18 @@ export default function ChatMemoryPanel(
|
|||||||
// 说明只给感知,不列出被影响的 agent 列表(内核不主动 prune,故也无需精确数字)。
|
// 说明只给感知,不列出被影响的 agent 列表(内核不主动 prune,故也无需精确数字)。
|
||||||
if (newScope === 'private') {
|
if (newScope === 'private') {
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: '设为私密后,其他 Agent 将不能再使用这条记忆',
|
message: 'Once set to private, other agents will no longer be able to use this memory',
|
||||||
description: '如需再次共享,随时可以改回团队可见。',
|
description: 'You can switch it back to team-visible again at any time.',
|
||||||
okText: '设为私密',
|
okText: 'Set to private',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await chatMemoryApi.patchScope(block.id, newScope);
|
await chatMemoryApi.patchScope(block.id, newScope);
|
||||||
tea.notify.success(newScope === 'team' ? '已切换为团队可见' : '已切换为私密');
|
tea.notify.success(newScope === 'team' ? 'Switched to team-visible' : 'Switched to private');
|
||||||
fetchBlocks();
|
fetchBlocks();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '切换可见范围失败');
|
tea.notify.error(e?.message || 'Failed to switch visibility');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,11 +434,11 @@ export default function ChatMemoryPanel(
|
|||||||
return (
|
return (
|
||||||
<div className="_asset-memory-page">
|
<div className="_asset-memory-page">
|
||||||
<AssetPageHeader
|
<AssetPageHeader
|
||||||
title="Chat_Memory · 原子记忆块"
|
title="Chat_Memory · Atomic memory blocks"
|
||||||
subtitle={
|
subtitle={
|
||||||
activeTeam
|
activeTeam
|
||||||
? `${activeTeam.name} · 共 ${blocks.length} 条记忆`
|
? `${activeTeam.name} · ${blocks.length} memories`
|
||||||
: `共 ${blocks.length} 条记忆`
|
: `${blocks.length} memories`
|
||||||
}
|
}
|
||||||
scope={
|
scope={
|
||||||
<Segment
|
<Segment
|
||||||
@ -458,10 +458,10 @@ export default function ChatMemoryPanel(
|
|||||||
value={agentFilter}
|
value={agentFilter}
|
||||||
onChange={setAgentFilter}
|
onChange={setAgentFilter}
|
||||||
disabled={ownedTeamAgents.length === 0}
|
disabled={ownedTeamAgents.length === 0}
|
||||||
placeholder="无可选 Agent"
|
placeholder="No agent available"
|
||||||
options={ownedTeamAgents.map((agent) => ({
|
options={ownedTeamAgents.map((agent) => ({
|
||||||
value: agent.agent_id,
|
value: agent.agent_id,
|
||||||
text: `${agent.name}(${agent.agent_id})`,
|
text: `${agent.name} (${agent.agent_id})`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
@ -475,18 +475,18 @@ export default function ChatMemoryPanel(
|
|||||||
selected.uploaded_by_user_id !== currentUserId;
|
selected.uploaded_by_user_id !== currentUserId;
|
||||||
const disabled = !selected || isPrivateAndNotOwner;
|
const disabled = !selected || isPrivateAndNotOwner;
|
||||||
const tooltip = !selected
|
const tooltip = !selected
|
||||||
? '请先选中一条记忆块'
|
? 'Select a memory block first'
|
||||||
: isPrivateAndNotOwner
|
: isPrivateAndNotOwner
|
||||||
? '该记忆已被 owner 设为私密,无法再分配给其他 Agent'
|
? 'This memory has been set to private by its owner and can no longer be allocated to other agents'
|
||||||
: undefined;
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => setShowAllocate(true)} disabled={disabled} tooltip={tooltip}>
|
<Button onClick={() => setShowAllocate(true)} disabled={disabled} tooltip={tooltip}>
|
||||||
分配到 Agent
|
Allocate to agent
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<Button type="primary" onClick={() => setShowImport(true)}>
|
<Button type="primary" onClick={() => setShowImport(true)}>
|
||||||
导入记忆
|
Import memory
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -507,14 +507,14 @@ export default function ChatMemoryPanel(
|
|||||||
<section className="_asset-memory-list-column">
|
<section className="_asset-memory-list-column">
|
||||||
<div className="_asset-memory-list-panel">
|
<div className="_asset-memory-list-panel">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="text-xs font-semibold text-foreground/85">记忆块</div>
|
<div className="text-xs font-semibold text-foreground/85">Memory blocks</div>
|
||||||
<div className="text-[11px] text-muted-foreground">
|
<div className="text-[11px] text-muted-foreground">
|
||||||
{filtered.length} / {blocks.length}
|
{filtered.length} / {blocks.length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="text-[12px] text-muted-foreground px-3 py-4">
|
<div className="text-[12px] text-muted-foreground px-3 py-4">
|
||||||
没有匹配的记忆块。
|
No matching memory blocks.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-0.5">
|
||||||
@ -541,7 +541,7 @@ export default function ChatMemoryPanel(
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
title={
|
title={
|
||||||
isRevoked
|
isRevoked
|
||||||
? '该记忆已被 owner 设为私密,不可预览;可点右侧"解绑"清理该绑定。'
|
? 'This memory has been set to private by its owner and can\'t be previewed; click "Unbind" on the right to clear this binding.'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -563,7 +563,7 @@ export default function ChatMemoryPanel(
|
|||||||
color: 'var(--tea-color-text-warning-default)',
|
color: 'var(--tea-color-text-warning-default)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
已被 owner 设为私密
|
Set to private by owner
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -576,7 +576,7 @@ export default function ChatMemoryPanel(
|
|||||||
borderColor: 'var(--tea-color-border-success-default)',
|
borderColor: 'var(--tea-color-border-success-default)',
|
||||||
color: 'var(--tea-color-text-success-default)',
|
color: 'var(--tea-color-text-success-default)',
|
||||||
}}
|
}}
|
||||||
title={`Agent 固定资产 · ${b.agent_id}`}
|
title={`Agent fixed asset · ${b.agent_id}`}
|
||||||
>
|
>
|
||||||
<AppIcon size={12} /> {agentLabel(b.agent_id)}
|
<AppIcon size={12} /> {agentLabel(b.agent_id)}
|
||||||
</span>
|
</span>
|
||||||
@ -588,20 +588,20 @@ export default function ChatMemoryPanel(
|
|||||||
borderColor: 'var(--tea-color-border-warning-default)',
|
borderColor: 'var(--tea-color-border-warning-default)',
|
||||||
color: 'var(--tea-color-text-warning-default)',
|
color: 'var(--tea-color-text-warning-default)',
|
||||||
}}
|
}}
|
||||||
title="团队记忆池"
|
title="Team memory pool"
|
||||||
>
|
>
|
||||||
<UsergroupIcon size={12} /> 团队池
|
<UsergroupIcon size={12} /> Team pool
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{b.uploaded_by_user_id && (
|
{b.uploaded_by_user_id && (
|
||||||
<div className="mt-1 text-[10px] text-muted-foreground">
|
<div className="mt-1 text-[10px] text-muted-foreground">
|
||||||
上传:
|
Uploaded by:
|
||||||
<span className="font-mono text-foreground/70">
|
<span className="font-mono text-foreground/70">
|
||||||
@{b.uploaded_by_user_id}
|
@{b.uploaded_by_user_id}
|
||||||
</span>
|
</span>
|
||||||
{b.uploaded_by_user_id === currentUserId && (
|
{b.uploaded_by_user_id === currentUserId && (
|
||||||
<span className="ml-1 text-[9px] text-primary">(你)</span>
|
<span className="ml-1 text-[9px] text-primary"> (you)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -627,7 +627,7 @@ export default function ChatMemoryPanel(
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteBlock(b.id);
|
handleDeleteBlock(b.id);
|
||||||
}}
|
}}
|
||||||
title="解除该 Agent 对这条记忆块的固定绑定(记忆本身保留)"
|
title="Remove this agent's fixed binding to this memory block (the memory itself is kept)"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '8px',
|
right: '8px',
|
||||||
@ -644,7 +644,7 @@ export default function ChatMemoryPanel(
|
|||||||
boxShadow: 'var(--tea-shadow-xs)',
|
boxShadow: 'var(--tea-shadow-xs)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
解绑
|
Unbind
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@ -660,7 +660,7 @@ export default function ChatMemoryPanel(
|
|||||||
<div className="_asset-memory-detail-panel">
|
<div className="_asset-memory-detail-panel">
|
||||||
{!selected ? (
|
{!selected ? (
|
||||||
<div className="text-[12px] text-muted-foreground px-2 py-6">
|
<div className="text-[12px] text-muted-foreground px-2 py-6">
|
||||||
请在左侧选中一个记忆块查看详情。
|
Select a memory block on the left to see details.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<BlockDetail
|
<BlockDetail
|
||||||
@ -701,11 +701,11 @@ export default function ChatMemoryPanel(
|
|||||||
onAllocated={async (agentId) => {
|
onAllocated={async (agentId) => {
|
||||||
try {
|
try {
|
||||||
await chatMemoryApi.allocate(activeTeamId!, selected.id, agentId);
|
await chatMemoryApi.allocate(activeTeamId!, selected.id, agentId);
|
||||||
tea.notify.success('已分配到 Agent');
|
tea.notify.success('Allocated to agent');
|
||||||
setShowAllocate(false);
|
setShowAllocate(false);
|
||||||
fetchBlocks();
|
fetchBlocks();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '分配失败');
|
tea.notify.error(e?.message || 'Allocation failed');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,8 +11,8 @@ interface ImportMessage {
|
|||||||
|
|
||||||
const MAX_MESSAGES = 100;
|
const MAX_MESSAGES = 100;
|
||||||
const SAMPLE_JSON = `[
|
const SAMPLE_JSON = `[
|
||||||
{ "role": "user", "content": "帮我 review 一下这段代码" },
|
{ "role": "user", "content": "Can you review this bit of code for me?" },
|
||||||
{ "role": "assistant", "content": "好的,请贴出来。" }
|
{ "role": "assistant", "content": "Sure, go ahead and paste it." }
|
||||||
]`;
|
]`;
|
||||||
|
|
||||||
type ParseResult =
|
type ParseResult =
|
||||||
@ -21,26 +21,26 @@ type ParseResult =
|
|||||||
|
|
||||||
function parseMessages(text: string): ParseResult {
|
function parseMessages(text: string): ParseResult {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (!trimmed) return { ok: false, error: '内容为空,请粘贴或上传 JSON。' };
|
if (!trimmed) return { ok: false, error: 'Content is empty — paste or upload JSON.' };
|
||||||
|
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try { parsed = JSON.parse(trimmed); } catch { return { ok: false, error: 'JSON 解析失败,请检查格式。' }; }
|
try { parsed = JSON.parse(trimmed); } catch { return { ok: false, error: 'Failed to parse JSON — check the format.' }; }
|
||||||
|
|
||||||
if (!Array.isArray(parsed)) return { ok: false, error: '根节点必须是 JSON 数组。' };
|
if (!Array.isArray(parsed)) return { ok: false, error: 'The root node must be a JSON array.' };
|
||||||
if (parsed.length === 0) return { ok: false, error: '消息数组不能为空。' };
|
if (parsed.length === 0) return { ok: false, error: 'The messages array cannot be empty.' };
|
||||||
if (parsed.length > MAX_MESSAGES) return { ok: false, error: `单次最多导入 ${MAX_MESSAGES} 条消息,当前 ${parsed.length} 条。` };
|
if (parsed.length > MAX_MESSAGES) return { ok: false, error: `A single import supports up to ${MAX_MESSAGES} messages; got ${parsed.length}.` };
|
||||||
|
|
||||||
const messages: ImportMessage[] = [];
|
const messages: ImportMessage[] = [];
|
||||||
for (let i = 0; i < parsed.length; i++) {
|
for (let i = 0; i < parsed.length; i++) {
|
||||||
const m = parsed[i] as any;
|
const m = parsed[i] as any;
|
||||||
if (!m || typeof m !== 'object') return { ok: false, error: `第 ${i + 1} 项不是对象。` };
|
if (!m || typeof m !== 'object') return { ok: false, error: `Item ${i + 1} is not an object.` };
|
||||||
const role = m.role;
|
const role = m.role;
|
||||||
if (role !== 'user' && role !== 'assistant') {
|
if (role !== 'user' && role !== 'assistant') {
|
||||||
return { ok: false, error: `第 ${i + 1} 项 role 必须是 "user" 或 "assistant",当前为 "${role}"。` };
|
return { ok: false, error: `Item ${i + 1}'s role must be "user" or "assistant", got "${role}".` };
|
||||||
}
|
}
|
||||||
const content = m.content;
|
const content = m.content;
|
||||||
if (typeof content !== 'string' || content.length === 0) {
|
if (typeof content !== 'string' || content.length === 0) {
|
||||||
return { ok: false, error: `第 ${i + 1} 项 content 必须是非空字符串。` };
|
return { ok: false, error: `Item ${i + 1}'s content must be a non-empty string.` };
|
||||||
}
|
}
|
||||||
messages.push({ role, content });
|
messages.push({ role, content });
|
||||||
}
|
}
|
||||||
@ -95,17 +95,17 @@ export function ImportBlockDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="导入记忆" size="l" onClose={onClose} disableEscape={submitting}>
|
<Modal visible caption="Import memory" size="l" onClose={onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Alert type="info">把一段历史对话(JSON 消息数组)作为 L0 导入到指定 agent,由系统自动蒸馏 L1/L2/L3。</Alert>
|
<Alert type="info">Import a slice of conversation history (JSON message array) as L0 into the given agent — the system will automatically distill L1/L2/L3.</Alert>
|
||||||
{/* 归属 agent */}
|
{/* 归属 agent */}
|
||||||
<Form layout="vertical" style={{ width: '100%' }}>
|
<Form layout="vertical" style={{ width: '100%' }}>
|
||||||
<Form.Item label="归属 Agent(必选)" extra="记忆将作为该 agent 的固定资产 —— 仅当 task 关联了该 agent 时才会被加载。">
|
<Form.Item label="Owning agent (required)" extra="The memory will become a fixed asset of this agent — it is only loaded when a task is linked to this agent.">
|
||||||
{agents.length === 0 ? (
|
{agents.length === 0 ? (
|
||||||
<Alert type="warning">当前 team 暂无 agent,无法导入。请先到团队管理中创建至少一个 agent。</Alert>
|
<Alert type="warning">This team has no agents yet, so import isn't possible. Create at least one agent in team management first.</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Select size="full" value={scopeAgentId} onChange={setScopeAgentId}
|
<Select size="full" value={scopeAgentId} onChange={setScopeAgentId}
|
||||||
options={agents.map((a) => ({ value: a.agent_id, text: `${a.name}(${a.agent_id})` }))} />
|
options={agents.map((a) => ({ value: a.agent_id, text: `${a.name} (${a.agent_id})` }))} />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
@ -113,11 +113,11 @@ export function ImportBlockDialog({
|
|||||||
{/* 格式说明 */}
|
{/* 格式说明 */}
|
||||||
<Alert type="info" style={{ marginTop: 12 }}>
|
<Alert type="info" style={{ marginTop: 12 }}>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div>支持 <code className="px-1 rounded text-[11px]" style={{ background: 'var(--tea-color-bg-secondary-default)' }}>[{`{role, content}`}]</code> 格式的 JSON 数组:</div>
|
<div>Supports a JSON array in the <code className="px-1 rounded text-[11px]" style={{ background: 'var(--tea-color-bg-secondary-default)' }}>[{`{role, content}`}]</code> format:</div>
|
||||||
<ul className="list-disc pl-5 text-[11px] space-y-0.5">
|
<ul className="list-disc pl-5 text-[11px] space-y-0.5">
|
||||||
<li><code className="text-[11px]">role</code> 取值:<code className="text-[11px]">"user"</code> 或 <code className="text-[11px]">"assistant"</code></li>
|
<li><code className="text-[11px]">role</code> is either <code className="text-[11px]">"user"</code> or <code className="text-[11px]">"assistant"</code></li>
|
||||||
<li><code className="text-[11px]">content</code>:消息正文(字符串,非空)</li>
|
<li><code className="text-[11px]">content</code>: message body (non-empty string)</li>
|
||||||
<li>单次最多 <strong>{MAX_MESSAGES}</strong> 条</li>
|
<li>Up to <strong>{MAX_MESSAGES}</strong> messages per import</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -125,12 +125,12 @@ export function ImportBlockDialog({
|
|||||||
{/* 导入方式切换 */}
|
{/* 导入方式切换 */}
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<Segment value={importMode} onChange={(v) => setImportMode(v as 'paste' | 'file')}
|
<Segment value={importMode} onChange={(v) => setImportMode(v as 'paste' | 'file')}
|
||||||
options={[{ value: 'paste', text: (<><FilePasteIcon size={12} /> 粘贴文本</>) }, { value: 'file', text: (<><UploadIcon size={12} /> 导入 JSON 文件</>) }]} />
|
options={[{ value: 'paste', text: (<><FilePasteIcon size={12} /> Paste text</>) }, { value: 'file', text: (<><UploadIcon size={12} /> Import JSON file</>) }]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form layout="vertical" style={{ width: '100%', marginTop: 12 }}>
|
<Form layout="vertical" style={{ width: '100%', marginTop: 12 }}>
|
||||||
{importMode === 'paste' ? (
|
{importMode === 'paste' ? (
|
||||||
<Form.Item label="Messages(JSON 数组)">
|
<Form.Item label="Messages (JSON array)">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={sessionPayload}
|
value={sessionPayload}
|
||||||
@ -142,13 +142,13 @@ export function ImportBlockDialog({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item label="选择 JSON 文件">
|
<Form.Item label="Select JSON file">
|
||||||
<Upload accept=".json,.txt,.md" beforeUpload={handleFilePicked}><Button>选择文件</Button></Upload>
|
<Upload accept=".json,.txt,.md" beforeUpload={handleFilePicked}><Button>Choose file</Button></Upload>
|
||||||
{fileName && <Text theme="text" parent="div" style={{ marginTop: 6 }}>已选择:<Text parent="code">{fileName}</Text></Text>}
|
{fileName && <Text theme="text" parent="div" style={{ marginTop: 6 }}>Selected: <Text parent="code">{fileName}</Text></Text>}
|
||||||
{sessionPayload && (
|
{sessionPayload && (
|
||||||
<Form.Item label="文件内容预览" style={{ marginTop: 8 }}>
|
<Form.Item label="File content preview" style={{ marginTop: 8 }}>
|
||||||
<pre className="w-full max-h-48 overflow-y-auto rounded-lg border bg-muted/50 px-2 py-1.5 text-[10px] font-mono text-foreground/70 whitespace-pre-wrap">
|
<pre className="w-full max-h-48 overflow-y-auto rounded-lg border bg-muted/50 px-2 py-1.5 text-[10px] font-mono text-foreground/70 whitespace-pre-wrap">
|
||||||
{sessionPayload.slice(0, 2000)}{sessionPayload.length > 2000 ? '\n…(已截断)' : ''}
|
{sessionPayload.slice(0, 2000)}{sessionPayload.length > 2000 ? '\n…(truncated)' : ''}
|
||||||
</pre>
|
</pre>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@ -159,7 +159,7 @@ export function ImportBlockDialog({
|
|||||||
{/* 解析结果反馈 */}
|
{/* 解析结果反馈 */}
|
||||||
{sessionPayload.trim() && parsed.ok && (
|
{sessionPayload.trim() && parsed.ok && (
|
||||||
<Alert type="success" style={{ marginTop: 12 }}>
|
<Alert type="success" style={{ marginTop: 12 }}>
|
||||||
解析成功 · 共 {parsed.messages.length} 条消息({parsed.messages.filter(m => m.role === 'user').length} user / {parsed.messages.filter(m => m.role === 'assistant').length} assistant)
|
Parsed successfully · {parsed.messages.length} messages total ({parsed.messages.filter(m => m.role === 'user').length} user / {parsed.messages.filter(m => m.role === 'assistant').length} assistant)
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{sessionPayload.trim() && !parsed.ok && (
|
{sessionPayload.trim() && !parsed.ok && (
|
||||||
@ -172,11 +172,11 @@ export function ImportBlockDialog({
|
|||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
title={!scopeAgentId ? '请先选择归属 agent' : !parsed.ok ? parsed.error : ''}
|
title={!scopeAgentId ? 'Select an owning agent first' : !parsed.ok ? parsed.error : ''}
|
||||||
>
|
>
|
||||||
{submitting ? '导入中…' : '导入记忆'}
|
{submitting ? 'Importing...' : 'Import memory'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} disabled={submitting}>取消</Button>
|
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,9 +22,9 @@ function MemoryOwnerTag({ userId, isCurrentUser }: { userId: string; isCurrentUs
|
|||||||
const displayName = useUserDisplayName(userId);
|
const displayName = useUserDisplayName(userId);
|
||||||
return (
|
return (
|
||||||
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-owner-tag">
|
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-owner-tag">
|
||||||
<span className="_cm-personal-tag-content" title={`owner user: ${displayName || userId}(${userId})`}>
|
<span className="_cm-personal-tag-content" title={`owner user: ${displayName || userId} (${userId})`}>
|
||||||
<UserIcon size={10} /> {displayName || userId}
|
<UserIcon size={10} /> {displayName || userId}
|
||||||
{isCurrentUser && '(你)'}
|
{isCurrentUser && ' (you)'}
|
||||||
</span>
|
</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
@ -53,20 +53,20 @@ export function PersonalAssetsTable({
|
|||||||
<Card.Body>
|
<Card.Body>
|
||||||
{/* 顶部 */}
|
{/* 顶部 */}
|
||||||
<div className="_cm-personal-header">
|
<div className="_cm-personal-header">
|
||||||
<Text theme="strong" parent="div">我的资产分配</Text>
|
<Text theme="strong" parent="div">My asset allocations</Text>
|
||||||
<Text theme="weak" parent="div" className="_cm-personal-header-desc">
|
<Text theme="weak" parent="div" className="_cm-personal-header-desc">
|
||||||
新建 Agent 时自动生成的记忆默认私密,切换为「共享」后团队内其他成员可见(只读)
|
Memories auto-generated for a new agent default to private; switch to "Shared" to make them visible (read-only) to other team members
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="_cm-personal-empty">
|
<div className="_cm-personal-empty">
|
||||||
<Text theme="weak">加载中…</Text>
|
<Text theme="weak">Loading...</Text>
|
||||||
</div>
|
</div>
|
||||||
) : blocks.length === 0 ? (
|
) : blocks.length === 0 ? (
|
||||||
<div className="_cm-personal-empty">
|
<div className="_cm-personal-empty">
|
||||||
<Text theme="weak" parent="div">
|
<Text theme="weak" parent="div">
|
||||||
暂无记忆资产 · 创建一个 Agent 后会自动生成一条属于它的私密记忆
|
No memory assets yet · creating an agent automatically generates a private memory for it
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -88,7 +88,7 @@ export function PersonalAssetsTable({
|
|||||||
<div className="_cm-personal-asset-main">
|
<div className="_cm-personal-asset-main">
|
||||||
<div className="_cm-personal-asset-name" title={block.title}>{block.title}</div>
|
<div className="_cm-personal-asset-name" title={block.title}>{block.title}</div>
|
||||||
<div className="_cm-personal-asset-meta">
|
<div className="_cm-personal-asset-meta">
|
||||||
更新时间:{new Date(block.updated_at_ms).toLocaleString()}
|
Updated: {new Date(block.updated_at_ms).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<div className="_cm-personal-asset-id" title={block.id}>{block.id}</div>
|
<div className="_cm-personal-asset-id" title={block.id}>{block.id}</div>
|
||||||
{block.uploaded_by_user_id && (
|
{block.uploaded_by_user_id && (
|
||||||
@ -96,11 +96,11 @@ export function PersonalAssetsTable({
|
|||||||
<MemoryOwnerTag userId={block.uploaded_by_user_id} isCurrentUser={ownerIsMe} />
|
<MemoryOwnerTag userId={block.uploaded_by_user_id} isCurrentUser={ownerIsMe} />
|
||||||
{isTeam ? (
|
{isTeam ? (
|
||||||
<Tag theme="success" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-state-tag">
|
<Tag theme="success" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-state-tag">
|
||||||
<span className="_cm-personal-tag-content"><ShareIcon size={10} /> 共享</span>
|
<span className="_cm-personal-tag-content"><ShareIcon size={10} /> Shared</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-state-tag">
|
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_cm-personal-state-tag">
|
||||||
<span className="_cm-personal-tag-content"><LockOnIcon size={10} /> 私密</span>
|
<span className="_cm-personal-tag-content"><LockOnIcon size={10} /> Private</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -114,16 +114,16 @@ export function PersonalAssetsTable({
|
|||||||
<Button
|
<Button
|
||||||
type={isTeam ? 'primary' : 'weak'}
|
type={isTeam ? 'primary' : 'weak'}
|
||||||
onClick={() => onToggleScope(block, 'team')}
|
onClick={() => onToggleScope(block, 'team')}
|
||||||
tooltip="team 内成员可读;owner 和 admin 可写"
|
tooltip="Readable by team members; writable by owner and admin"
|
||||||
>
|
>
|
||||||
<ShareIcon size={12} /> 共享
|
<ShareIcon size={12} /> Shared
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type={isPrivate ? 'primary' : 'weak'}
|
type={isPrivate ? 'primary' : 'weak'}
|
||||||
onClick={() => onToggleScope(block, 'private')}
|
onClick={() => onToggleScope(block, 'private')}
|
||||||
tooltip="只有 owner 和 team admin 能看到"
|
tooltip="Visible only to the owner and team admin"
|
||||||
>
|
>
|
||||||
<LockOnIcon size={12} /> 私密
|
<LockOnIcon size={12} /> Private
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,16 +4,16 @@ export const PROSE_CLASS =
|
|||||||
'prose prose-sm prose-slate max-w-none prose-headings:my-2 prose-p:my-1.5 prose-ul:my-1.5 prose-ol:my-1.5 prose-pre:my-1.5';
|
'prose prose-sm prose-slate max-w-none prose-headings:my-2 prose-p:my-1.5 prose-ul:my-1.5 prose-ol:my-1.5 prose-pre:my-1.5';
|
||||||
|
|
||||||
export const LAYERS: LayerMeta[] = [
|
export const LAYERS: LayerMeta[] = [
|
||||||
{ id: 'L0', label: 'L0 · 对话原文', short: '对话原文', desc: '原始对话 / 工具调用流水,不做压缩', tone: 'default' },
|
{ id: 'L0', label: 'L0 · Raw conversation', short: 'Raw conversation', desc: 'Raw conversation / tool-call log, uncompressed', tone: 'default' },
|
||||||
{ id: 'L1', label: 'L1 · 原子记忆', short: '原子记忆', desc: '从原文抽取出来的最小事实 / 约束', tone: 'brand' },
|
{ id: 'L1', label: 'L1 · Atomic memory', short: 'Atomic memory', desc: 'Minimal facts / constraints extracted from the raw text', tone: 'brand' },
|
||||||
{ id: 'L2', label: 'L2 · 场景记忆', short: '场景记忆', desc: '围绕场景聚合的多条原子记忆总结', tone: 'success' },
|
{ id: 'L2', label: 'L2 · Scene memory', short: 'Scene memory', desc: 'Summary aggregating multiple atomic memories around a scene', tone: 'success' },
|
||||||
{ id: 'L3', label: 'L3 · 核心记忆', short: '核心记忆', desc: '沉淀的核心准则 / 模板 / 决策', tone: 'warning' },
|
{ id: 'L3', label: 'L3 · Core memory', short: 'Core memory', desc: 'Distilled core principles / templates / decisions', tone: 'warning' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SCOPE_TAB_LABELS: Record<ScopeTab, string> = {
|
export const SCOPE_TAB_LABELS: Record<ScopeTab, string> = {
|
||||||
all: '全部',
|
all: 'All',
|
||||||
team: '团队资产',
|
team: 'Team assets',
|
||||||
fixed: 'Agent 资产',
|
fixed: 'Agent assets',
|
||||||
scope: '可分配资产',
|
scope: 'Allocatable assets',
|
||||||
personal: '我的资产分配',
|
personal: 'My asset allocations',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function formatShortTime(ms: number): string {
|
|||||||
yesterday.getFullYear() === d.getFullYear() &&
|
yesterday.getFullYear() === d.getFullYear() &&
|
||||||
yesterday.getMonth() === d.getMonth() &&
|
yesterday.getMonth() === d.getMonth() &&
|
||||||
yesterday.getDate() === d.getDate()
|
yesterday.getDate() === d.getDate()
|
||||||
) return '昨天';
|
) return 'Yesterday';
|
||||||
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export default function ForkSkillDialog(props: {
|
|||||||
|
|
||||||
async function submit(): Promise<void> {
|
async function submit(): Promise<void> {
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
setError('请选择 agent。');
|
setError('Select an agent.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -86,7 +86,7 @@ export default function ForkSkillDialog(props: {
|
|||||||
});
|
});
|
||||||
if (existing.items.some((s) => s.name === newName)) {
|
if (existing.items.some((s) => s.name === newName)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Agent "${agentId}" 下已存在同名 skill "${newName}"(单个 agent 不允许重名)。请先删除旧副本再重试。`
|
`Agent "${agentId}" already has a skill named "${newName}" (an agent can't have duplicate names). Delete the old copy first, then retry.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,11 +127,11 @@ export default function ForkSkillDialog(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resourceInfo = resources.length > 0
|
const resourceInfo = resources.length > 0
|
||||||
? `(已复制 ${resources.length} 个资源文件)`
|
? ` (copied ${resources.length} resource files)`
|
||||||
: (full.manifest?.length ?? 0) > 0
|
: (full.manifest?.length ?? 0) > 0
|
||||||
? `(注意:原 skill 有 ${full.manifest?.length} 个资源文件,复制均失败,如需请手动重新 import)`
|
? ` (note: the original skill has ${full.manifest?.length} resource files, all failed to copy — re-import manually if needed)`
|
||||||
: '';
|
: '';
|
||||||
setSuccess(`已 fork "${props.skillName}" @ ${agentId}${resourceInfo}`);
|
setSuccess(`Forked "${props.skillName}" @ ${agentId}${resourceInfo}`);
|
||||||
setTimeout(() => props.onForked(created), 800);
|
setTimeout(() => props.onForked(created), 800);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
@ -141,27 +141,27 @@ export default function ForkSkillDialog(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="Fork Skill 给 Agent" size="s" onClose={props.onClose} disableEscape={submitting}>
|
<Modal visible caption="Fork skill to agent" size="s" onClose={props.onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="说明">
|
<Form.Item label="Description">
|
||||||
<Form.Text>将 {props.skillName} 复制一份给所选 agent。副本与源 skill 解耦,agent 之后可独立修改副本。可在下方自定义副本名。</Form.Text>
|
<Form.Text>Copy {props.skillName} to the selected agent. The copy is decoupled from the source skill, and the agent can edit it independently afterward. You can customize the copy's name below.</Form.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Agent" required>
|
<Form.Item label="Agent" required>
|
||||||
<Select
|
<Select
|
||||||
size="full"
|
size="full"
|
||||||
value={agentId}
|
value={agentId}
|
||||||
onChange={setAgentId}
|
onChange={setAgentId}
|
||||||
placeholder="请选择 agent"
|
placeholder="Select an agent"
|
||||||
options={props.agents.map((a) => ({ value: a.id, text: `${a.id} · ${a.name}` }))}
|
options={props.agents.map((a) => ({ value: a.id, text: `${a.id} · ${a.name}` }))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="副本名" required>
|
<Form.Item label="Copy name" required>
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={setNewName}
|
onChange={setNewName}
|
||||||
placeholder="副本 skill 名称"
|
placeholder="Name for the copy"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
||||||
@ -170,7 +170,7 @@ export default function ForkSkillDialog(props: {
|
|||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId || !newName.trim()} loading={submitting}>Fork</Button>
|
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId || !newName.trim()} loading={submitting}>Fork</Button>
|
||||||
<Button onClick={props.onClose} disabled={submitting}>取消</Button>
|
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -101,7 +101,7 @@ function partitionFiles(files: File[]): {
|
|||||||
skillName: null,
|
skillName: null,
|
||||||
mainFile: null,
|
mainFile: null,
|
||||||
resources: [],
|
resources: [],
|
||||||
warning: '目录中找不到 SKILL.md。请确保至少有一个 SKILL.md 在根目录或 <skill-name>/ 下。'
|
warning: 'No SKILL.md found in the directory. Make sure there is at least one SKILL.md at the root or under <skill-name>/.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mainSegments = mainRelPath.split('/');
|
const mainSegments = mainRelPath.split('/');
|
||||||
@ -159,30 +159,30 @@ export default function ImportSkillDialog(props: {
|
|||||||
try {
|
try {
|
||||||
const agentId = props.target === 'fixed' ? (selectedAgentId || props.agentId || '') : '';
|
const agentId = props.target === 'fixed' ? (selectedAgentId || props.agentId || '') : '';
|
||||||
if (props.target === 'fixed' && !agentId) {
|
if (props.target === 'fixed' && !agentId) {
|
||||||
throw new Error('请选择归属 Agent。');
|
throw new Error('Select an owning agent.');
|
||||||
}
|
}
|
||||||
if (!props.teamId) throw new Error('缺少 team 上下文,无法导入。');
|
if (!props.teamId) throw new Error('Missing team context — cannot import.');
|
||||||
|
|
||||||
// ==== 对话导入:直接调 skill/extract ====
|
// ==== 对话导入:直接调 skill/extract ====
|
||||||
if (mode === 'session') {
|
if (mode === 'session') {
|
||||||
const raw = sessionPayload.trim();
|
const raw = sessionPayload.trim();
|
||||||
if (!raw) throw new Error('请粘贴对话 JSON。');
|
if (!raw) throw new Error('Paste the conversation JSON.');
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(raw);
|
parsed = JSON.parse(raw);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`对话 JSON 解析失败:${e instanceof Error ? e.message : String(e)}`);
|
throw new Error(`Failed to parse conversation JSON: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
}
|
}
|
||||||
if (!Array.isArray((parsed as { messages?: unknown }).messages)) {
|
if (!Array.isArray((parsed as { messages?: unknown }).messages)) {
|
||||||
throw new Error('对话 JSON 缺少 messages 数组字段。');
|
throw new Error('The conversation JSON is missing the messages array field.');
|
||||||
}
|
}
|
||||||
const msgs = (parsed as { messages: unknown[] }).messages;
|
const msgs = (parsed as { messages: unknown[] }).messages;
|
||||||
if (msgs.length === 0) {
|
if (msgs.length === 0) {
|
||||||
throw new Error('对话 messages 不能为空。');
|
throw new Error('Conversation messages cannot be empty.');
|
||||||
}
|
}
|
||||||
// 后端 extract 接口限制 messages 最多 500 条(见 iWiki §3.13),
|
// 后端 extract 接口限制 messages 最多 500 条(见 iWiki §3.13),
|
||||||
if (msgs.length > 500) {
|
if (msgs.length > 500) {
|
||||||
throw new Error(`对话消息过多(${msgs.length} 条),接口最多支持 500 条,请删减后重试。`);
|
throw new Error(`Too many conversation messages (${msgs.length}); the API supports up to 500 — trim and try again.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组装 extract 入参。身份字段(user_id/team_id/agent_id)强制用当前 UI
|
// 组装 extract 入参。身份字段(user_id/team_id/agent_id)强制用当前 UI
|
||||||
@ -238,18 +238,18 @@ export default function ImportSkillDialog(props: {
|
|||||||
if (raced === 'timeout') {
|
if (raced === 'timeout') {
|
||||||
softTimedOut = true;
|
softTimedOut = true;
|
||||||
setResult(
|
setResult(
|
||||||
'提交成功,提取任务已受理。预计 1-3 分钟后完成,请稍后刷新 skill 列表查看结果。',
|
'Submitted successfully — the extraction task has been accepted. It should finish in 1-3 minutes; refresh the skill list later to see the result.',
|
||||||
);
|
);
|
||||||
} else if (raced) {
|
} else if (raced) {
|
||||||
setResult(
|
setResult(
|
||||||
'提交成功,提取任务已受理。预计 1-3 分钟后完成,请稍后刷新 skill 列表查看结果。'
|
'Submitted successfully — the extraction task has been accepted. It should finish in 1-3 minutes; refresh the skill list later to see the result.'
|
||||||
+ `\n任务 ID:${raced.task_id}`,
|
+ `\nTask ID: ${raced.task_id}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// extractPromise 被软超时后 catch 成 null 的分支(正常不会走到这里,
|
// extractPromise 被软超时后 catch 成 null 的分支(正常不会走到这里,
|
||||||
// 因为软超时已经先设过 result 了),兜底保持一致文案。
|
// 因为软超时已经先设过 result 了),兜底保持一致文案。
|
||||||
setResult(
|
setResult(
|
||||||
'提交成功,提取任务已受理。预计 1-3 分钟后完成,请稍后刷新 skill 列表查看结果。',
|
'Submitted successfully — the extraction task has been accepted. It should finish in 1-3 minutes; refresh the skill list later to see the result.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setTimeout(() => props.onImported(), 1500);
|
setTimeout(() => props.onImported(), 1500);
|
||||||
@ -258,14 +258,14 @@ export default function ImportSkillDialog(props: {
|
|||||||
|
|
||||||
// ==== 目录导入:走 v3 create + files/write ====
|
// ==== 目录导入:走 v3 create + files/write ====
|
||||||
if (!partition?.mainFile) {
|
if (!partition?.mainFile) {
|
||||||
throw new Error(partition?.warning ?? '请选择包含 SKILL.md 的目录。');
|
throw new Error(partition?.warning ?? 'Select a directory that contains SKILL.md.');
|
||||||
}
|
}
|
||||||
const content = await readAsUtf8(partition.mainFile);
|
const content = await readAsUtf8(partition.mainFile);
|
||||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
const nameMatch = fmMatch?.[1].match(/^name:\s*(.+)$/m);
|
const nameMatch = fmMatch?.[1].match(/^name:\s*(.+)$/m);
|
||||||
const name = nameMatch?.[1].trim().replace(/^["']|["']$/g, '') || partition.skillName || '';
|
const name = nameMatch?.[1].trim().replace(/^["']|["']$/g, '') || partition.skillName || '';
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('无法从 SKILL.md 或目录结构推断 skill 名称,请检查 frontmatter 或目录布局。');
|
throw new Error('Could not infer a skill name from SKILL.md or the directory structure — check the frontmatter or directory layout.');
|
||||||
}
|
}
|
||||||
const resourceFiles: { path: string; file: File; isBinary: boolean }[] = partition.resources.map(
|
const resourceFiles: { path: string; file: File; isBinary: boolean }[] = partition.resources.map(
|
||||||
({ path, file }) => ({ path, file, isBinary: !looksLikeText(file) }),
|
({ path, file }) => ({ path, file, isBinary: !looksLikeText(file) }),
|
||||||
@ -297,7 +297,7 @@ export default function ImportSkillDialog(props: {
|
|||||||
resources: resources.length ? resources : undefined,
|
resources: resources.length ? resources : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setResult(`导入成功:${name}(${resourceFiles.length} 个资源文件)`);
|
setResult(`Imported successfully: ${name} (${resourceFiles.length} resource files)`);
|
||||||
setTimeout(() => props.onImported(), 800);
|
setTimeout(() => props.onImported(), 800);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
@ -312,27 +312,27 @@ export default function ImportSkillDialog(props: {
|
|||||||
const showAgentPicker = props.target === 'fixed' && !!props.agents;
|
const showAgentPicker = props.target === 'fixed' && !!props.agents;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="导入 Skill" size="l" onClose={props.onClose} disableEscape={submitting}>
|
<Modal visible caption="Import skill" size="l" onClose={props.onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Alert type="info">上传 SKILL.md 目录,或粘贴一段对话让系统自动提炼出 skill。</Alert>
|
<Alert type="info">Upload a SKILL.md directory, or paste a conversation to let the system automatically distill a skill.</Alert>
|
||||||
{/* 归属 Agent —— 必选项。始终展示在导入方式上方,
|
{/* 归属 Agent —— 必选项。始终展示在导入方式上方,
|
||||||
与 ChatMemoryPanel.ImportBlockDialog 一致:即便外层已经传了 agentId
|
与 ChatMemoryPanel.ImportBlockDialog 一致:即便外层已经传了 agentId
|
||||||
也允许在弹窗里重选。 */}
|
也允许在弹窗里重选。 */}
|
||||||
{showAgentPicker && (
|
{showAgentPicker && (
|
||||||
<Form layout="vertical" style={{ width: '100%' }}>
|
<Form layout="vertical" style={{ width: '100%' }}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="归属 Agent(必选)"
|
label="Owning agent (required)"
|
||||||
extra="Skill 将作为该 agent 的固定资产 —— 仅当 task 关联了该 agent 时才会被加载。"
|
extra="The skill will become a fixed asset of this agent — it is only loaded when a task is linked to this agent."
|
||||||
>
|
>
|
||||||
{props.agents!.length === 0 ? (
|
{props.agents!.length === 0 ? (
|
||||||
<Alert type="warning">当前 team 暂无 agent,无法导入。请先到团队管理中创建至少一个 agent。</Alert>
|
<Alert type="warning">This team has no agents yet, so import isn't possible. Create at least one agent in team management first.</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
size="full"
|
size="full"
|
||||||
value={selectedAgentId}
|
value={selectedAgentId}
|
||||||
onChange={setSelectedAgentId}
|
onChange={setSelectedAgentId}
|
||||||
placeholder="-- 选择 Agent --"
|
placeholder="-- Select agent --"
|
||||||
options={props.agents!.map((a) => ({ value: a.id, text: `${a.name}(${a.id})` }))}
|
options={props.agents!.map((a) => ({ value: a.id, text: `${a.name} (${a.id})` }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -345,14 +345,14 @@ export default function ImportSkillDialog(props: {
|
|||||||
value={mode}
|
value={mode}
|
||||||
onChange={(v) => setMode(v as Mode)}
|
onChange={(v) => setMode(v as Mode)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'directory', text: '目录导入' },
|
{ value: 'directory', text: 'Directory import' },
|
||||||
{ value: 'session', text: '对话导入' },
|
{ value: 'session', text: 'Conversation import' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{mode === 'directory' && (
|
{mode === 'directory' && (
|
||||||
<div className="_memory-isd-section">
|
<div className="_memory-isd-section">
|
||||||
<div className="_memory-isd-label">选择本地目录(应包含 SKILL.md 和可选的 files/ 子目录)</div>
|
<div className="_memory-isd-label">Choose a local directory (should contain SKILL.md and an optional files/ subdirectory)</div>
|
||||||
{/*
|
{/*
|
||||||
目录选择无 Tea 组件等价物,此处保留原生 input 作为唯一豁免,隐藏后由下方
|
目录选择无 Tea 组件等价物,此处保留原生 input 作为唯一豁免,隐藏后由下方
|
||||||
Tea Button 触发点击。webkitdirectory/directory 是非标准但广泛支持的浏览器
|
Tea Button 触发点击。webkitdirectory/directory 是非标准但广泛支持的浏览器
|
||||||
@ -371,24 +371,24 @@ export default function ImportSkillDialog(props: {
|
|||||||
{...({ webkitdirectory: '', directory: '' } as Record<string, string>)}
|
{...({ webkitdirectory: '', directory: '' } as Record<string, string>)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => fileInputRef.current?.click()}>
|
<Button onClick={() => fileInputRef.current?.click()}>
|
||||||
<FolderOpenIcon size={14} /> 选择文件夹
|
<FolderOpenIcon size={14} /> Choose folder
|
||||||
</Button>
|
</Button>
|
||||||
{pickedFiles.length > 0 && (
|
{pickedFiles.length > 0 && (
|
||||||
<span className="_memory-isd-picked-count">已选择 {pickedFiles.length} 个文件</span>
|
<span className="_memory-isd-picked-count">{pickedFiles.length} files selected</span>
|
||||||
)}
|
)}
|
||||||
{pickedFiles.length > 0 && partition && (
|
{pickedFiles.length > 0 && partition && (
|
||||||
<div className="_memory-isd-partition-box">
|
<div className="_memory-isd-partition-box">
|
||||||
<div>
|
<div>
|
||||||
主文件:
|
Main file:
|
||||||
{partition.mainFile ? (
|
{partition.mainFile ? (
|
||||||
<span className="_memory-isd-main-file">
|
<span className="_memory-isd-main-file">
|
||||||
{partition.mainFile.webkitRelativePath || partition.mainFile.name}
|
{partition.mainFile.webkitRelativePath || partition.mainFile.name}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="_memory-isd-error-text">未找到 SKILL.md</span>
|
<span className="_memory-isd-error-text">SKILL.md not found</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>资源文件:{partition.resources.length} 个</div>
|
<div>Resource files: {partition.resources.length}</div>
|
||||||
{partition.resources.length > 0 && (
|
{partition.resources.length > 0 && (
|
||||||
<ul className="_memory-isd-resource-list">
|
<ul className="_memory-isd-resource-list">
|
||||||
{partition.resources.map((r) => (
|
{partition.resources.map((r) => (
|
||||||
@ -407,11 +407,11 @@ export default function ImportSkillDialog(props: {
|
|||||||
{mode === 'session' && (
|
{mode === 'session' && (
|
||||||
<div className="_memory-isd-section">
|
<div className="_memory-isd-section">
|
||||||
<Alert type="info">
|
<Alert type="info">
|
||||||
粘贴一段与该 agent 的对话,系统会自动从中提炼可复用的 skill 沉淀到该 agent 下。
|
Paste a conversation with this agent — the system will automatically distill a reusable skill for it.
|
||||||
对话 JSON 需包含 <span className="_memory-isd-mono-inline">messages</span> 数组。
|
The conversation JSON must include a <span className="_memory-isd-mono-inline">messages</span> array.
|
||||||
</Alert>
|
</Alert>
|
||||||
<Form layout="vertical" style={{ width: '100%' }}>
|
<Form layout="vertical" style={{ width: '100%' }}>
|
||||||
<Form.Item label="对话 JSON">
|
<Form.Item label="Conversation JSON">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={sessionPayload}
|
value={sessionPayload}
|
||||||
@ -422,11 +422,11 @@ export default function ImportSkillDialog(props: {
|
|||||||
session_id: 'demo-user-extract-demo-1',
|
session_id: 'demo-user-extract-demo-1',
|
||||||
task_id: 'default',
|
task_id: 'default',
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'user', content: '我们的 PostgreSQL 14 主库今天又卡死了…' },
|
{ role: 'user', content: 'Our PostgreSQL 14 primary froze again today…' },
|
||||||
{ role: 'assistant', content: '先 ssh 到主库节点,查看慢查询日志…' },
|
{ role: 'assistant', content: 'Let me SSH into the primary node first and check the slow query log…' },
|
||||||
{ role: 'tool_call', content: '调用 bash 执行: tail -100 /var/log/postgresql/slow.log' },
|
{ role: 'tool_call', content: 'Calling bash to run: tail -100 /var/log/postgresql/slow.log' },
|
||||||
{ role: 'tool_result', content: 'Query duration: 120s | SELECT * FROM large_table WHERE ...' },
|
{ role: 'tool_result', content: 'Query duration: 120s | SELECT * FROM large_table WHERE ...' },
|
||||||
{ role: 'assistant', content: '发现一条慢查询耗时 120s,建议添加索引…' },
|
{ role: 'assistant', content: 'Found a slow query taking 120s — recommend adding an index…' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@ -451,12 +451,12 @@ export default function ImportSkillDialog(props: {
|
|||||||
|| (mode === 'directory' ? !partition?.mainFile : !sessionPayload.trim())
|
|| (mode === 'directory' ? !partition?.mainFile : !sessionPayload.trim())
|
||||||
|| (props.target === 'fixed' && !selectedAgentId)
|
|| (props.target === 'fixed' && !selectedAgentId)
|
||||||
}
|
}
|
||||||
title={props.target === 'fixed' && !selectedAgentId ? '请先选择归属 agent' : ''}
|
title={props.target === 'fixed' && !selectedAgentId ? 'Select an owning agent first' : ''}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
>
|
>
|
||||||
{mode === 'session' ? '开始提取' : '导入 Skill'}
|
{mode === 'session' ? 'Start extraction' : 'Import skill'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={props.onClose} disabled={submitting}>取消</Button>
|
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFilePreview({
|
setFilePreview({
|
||||||
path,
|
path,
|
||||||
content: `读取失败:${err instanceof Error ? err.message : String(err)}`,
|
content: `Failed to read: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
size_bytes: 0,
|
size_bytes: 0,
|
||||||
mime_type: 'text/plain',
|
mime_type: 'text/plain',
|
||||||
@ -149,7 +149,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
|
|||||||
return (
|
return (
|
||||||
<Card className="_memory-skill-detail-card">
|
<Card className="_memory-skill-detail-card">
|
||||||
<Card.Body className="_memory-skill-detail-empty">
|
<Card.Body className="_memory-skill-detail-empty">
|
||||||
<Text theme="weak">在左侧选中一条 skill 查看详情。</Text>
|
<Text theme="weak">Select a skill on the left to see details.</Text>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@ -168,7 +168,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
|
|||||||
{!stale && error && (
|
{!stale && error && (
|
||||||
<Text theme="danger" parent="div" className="_memory-skill-detail-error">{error}</Text>
|
<Text theme="danger" parent="div" className="_memory-skill-detail-error">{error}</Text>
|
||||||
)}
|
)}
|
||||||
{showLoading && <Text theme="weak" parent="div">加载中…</Text>}
|
{showLoading && <Text theme="weak" parent="div">Loading...</Text>}
|
||||||
{currentView && (
|
{currentView && (
|
||||||
<>
|
<>
|
||||||
{/* Metadata */}
|
{/* Metadata */}
|
||||||
@ -204,10 +204,10 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
|
|||||||
{/* Files */}
|
{/* Files */}
|
||||||
<div className="_memory-skill-detail-section">
|
<div className="_memory-skill-detail-section">
|
||||||
<Text theme="label" parent="div" className="_memory-skill-detail-section-title">
|
<Text theme="label" parent="div" className="_memory-skill-detail-section-title">
|
||||||
附属资源 ({currentView.manifest?.length ?? 0})
|
Attached resources ({currentView.manifest?.length ?? 0})
|
||||||
</Text>
|
</Text>
|
||||||
{!currentView.manifest || currentView.manifest.length === 0 ? (
|
{!currentView.manifest || currentView.manifest.length === 0 ? (
|
||||||
<Text theme="weak" parent="div">无附属文件。</Text>
|
<Text theme="weak" parent="div">No attached files.</Text>
|
||||||
) : (
|
) : (
|
||||||
<div className="_memory-skill-files-box">
|
<div className="_memory-skill-files-box">
|
||||||
<FileTreeView nodes={fileTree} onPick={pickFile} />
|
<FileTreeView nodes={fileTree} onPick={pickFile} />
|
||||||
@ -223,10 +223,10 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
|
|||||||
<Modal visible caption={filePreview.path} size="xl" onClose={() => setFilePreview(null)}>
|
<Modal visible caption={filePreview.path} size="xl" onClose={() => setFilePreview(null)}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{filePreviewLoading ? (
|
{filePreviewLoading ? (
|
||||||
<Text theme="weak" parent="div">加载中…</Text>
|
<Text theme="weak" parent="div">Loading...</Text>
|
||||||
) : filePreview.encoding === 'base64' ? (
|
) : filePreview.encoding === 'base64' ? (
|
||||||
<Text theme="weak" parent="div">
|
<Text theme="weak" parent="div">
|
||||||
二进制文件 ({filePreview.size_bytes} bytes),base64 已省略。
|
Binary file ({filePreview.size_bytes} bytes) — base64 content omitted.
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<pre className="_memory-skill-file-content">{filePreview.content}</pre>
|
<pre className="_memory-skill-file-content">{filePreview.content}</pre>
|
||||||
|
|||||||
@ -46,20 +46,20 @@ import './skills-list.css';
|
|||||||
type Tab = 'team' | 'fixed' | 'personal';
|
type Tab = 'team' | 'fixed' | 'personal';
|
||||||
|
|
||||||
const TAB_LABELS: Record<Tab, string> = {
|
const TAB_LABELS: Record<Tab, string> = {
|
||||||
team: '团队资产',
|
team: 'Team assets',
|
||||||
fixed: 'Agent 资产',
|
fixed: 'Agent assets',
|
||||||
personal: '我的资产分配',
|
personal: 'My asset allocations',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 统一 Skill 列表的用户归属徽章:优先展示 display_name,再回退 user_id。 */
|
/** 统一 Skill 列表的用户归属徽章:优先展示 display_name,再回退 user_id。 */
|
||||||
function SkillOwnerTag({ userId, isCurrentUser }: { userId: string; isCurrentUser: boolean }) {
|
function SkillOwnerTag({ userId, isCurrentUser }: { userId: string; isCurrentUser: boolean }) {
|
||||||
const displayName = useUserDisplayName(userId);
|
const displayName = useUserDisplayName(userId);
|
||||||
return (
|
return (
|
||||||
<span title={`owner user: ${displayName || userId}(${userId})`}>
|
<span title={`owner user: ${displayName || userId} (${userId})`}>
|
||||||
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
||||||
<span className="_memory-skill-tag-content">
|
<span className="_memory-skill-tag-content">
|
||||||
<UserIcon size={10} /> {displayName || userId}
|
<UserIcon size={10} /> {displayName || userId}
|
||||||
{isCurrentUser && '(你)'}
|
{isCurrentUser && ' (you)'}
|
||||||
</span>
|
</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
</span>
|
</span>
|
||||||
@ -110,7 +110,7 @@ export default function SkillsPanel({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
// agent 加载失败不致命(列表 fallback 显示 agent_id),但仍给出提示。
|
// agent 加载失败不致命(列表 fallback 显示 agent_id),但仍给出提示。
|
||||||
tea.notify.error(err?.message || '加载 Agent 信息失败');
|
tea.notify.error(err?.message || 'Failed to load agent info');
|
||||||
setAgentNameMap({});
|
setAgentNameMap({});
|
||||||
setTeamAgents([]);
|
setTeamAgents([]);
|
||||||
});
|
});
|
||||||
@ -345,7 +345,7 @@ export default function SkillsPanel({
|
|||||||
if (selectedSkillId === skill.skill_id) {
|
if (selectedSkillId === skill.skill_id) {
|
||||||
setSelectedSkillId(null);
|
setSelectedSkillId(null);
|
||||||
}
|
}
|
||||||
tea.notify.success(`已删除 Skill「${skill.name}」`);
|
tea.notify.success(`Deleted skill "${skill.name}"`);
|
||||||
void refresh();
|
void refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tea.notify.error(err);
|
tea.notify.error(err);
|
||||||
@ -362,10 +362,10 @@ export default function SkillsPanel({
|
|||||||
<div className="_memory-skills-body">
|
<div className="_memory-skills-body">
|
||||||
{/* 固定资产的 Agent 选择器与 Code 页 "Agent 资产" 选项栏保持相同呈现。 */}
|
{/* 固定资产的 Agent 选择器与 Code 页 "Agent 资产" 选项栏保持相同呈现。 */}
|
||||||
<AssetPageHeader
|
<AssetPageHeader
|
||||||
title="Skill 资产管理"
|
title="Skill asset management"
|
||||||
subtitle={activeTeam
|
subtitle={activeTeam
|
||||||
? `${activeTeam.name} · 共 ${tab === 'personal' ? '我的' : ''}${skills.length} 个 Skill`
|
? `${activeTeam.name} · ${tab === 'personal' ? 'My ' : ''}${skills.length} skills`
|
||||||
: `共 ${tab === 'personal' ? '我的' : ''}${skills.length} 个 Skill`}
|
: `${tab === 'personal' ? 'My ' : ''}${skills.length} skills`}
|
||||||
scope={(
|
scope={(
|
||||||
<Segment
|
<Segment
|
||||||
value={tab}
|
value={tab}
|
||||||
@ -380,8 +380,8 @@ export default function SkillsPanel({
|
|||||||
value={selectedAgent}
|
value={selectedAgent}
|
||||||
onChange={(value) => { setSelectedAgent(value); setSelectedSkillId(null); }}
|
onChange={(value) => { setSelectedAgent(value); setSelectedSkillId(null); }}
|
||||||
disabled={teamAgents.length === 0}
|
disabled={teamAgents.length === 0}
|
||||||
placeholder="无可选 Agent"
|
placeholder="No agent available"
|
||||||
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name}(${agent.id})` }))}
|
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name} (${agent.id})` }))}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
actions={(
|
actions={(
|
||||||
@ -391,21 +391,21 @@ export default function SkillsPanel({
|
|||||||
const forkableInPersonal = tab === 'personal' && !!selectedPersonalAsset;
|
const forkableInPersonal = tab === 'personal' && !!selectedPersonalAsset;
|
||||||
const canFork = forkableInTeam || forkableInPersonal;
|
const canFork = forkableInTeam || forkableInPersonal;
|
||||||
const tooltip = tab === 'fixed'
|
const tooltip = tab === 'fixed'
|
||||||
? '请在「团队」或「我的资产分配」视图选中一条 skill'
|
? 'Select a skill in the "Team" or "My asset allocations" view'
|
||||||
: tab === 'team'
|
: tab === 'team'
|
||||||
? (!selectedSkillId ? '请先选中一条 skill' : undefined)
|
? (!selectedSkillId ? 'Select a skill first' : undefined)
|
||||||
: (!selectedPersonalAsset ? '请先选中一条 skill' : undefined);
|
: (!selectedPersonalAsset ? 'Select a skill first' : undefined);
|
||||||
return tab === 'fixed' ? null : (
|
return tab === 'fixed' ? null : (
|
||||||
<Button onClick={() => setShowFork(true)} disabled={!canFork} tooltip={tooltip}>Fork(可写)</Button>
|
<Button onClick={() => setShowFork(true)} disabled={!canFork} tooltip={tooltip}>Fork (writable)</Button>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setShowImport(true)}
|
onClick={() => setShowImport(true)}
|
||||||
disabled={teamAgents.length === 0}
|
disabled={teamAgents.length === 0}
|
||||||
tooltip={teamAgents.length === 0 ? '当前 team 暂无 agent,请先创建 agent' : undefined}
|
tooltip={teamAgents.length === 0 ? 'This team has no agents yet — create one first' : undefined}
|
||||||
>
|
>
|
||||||
导入 Skill
|
Import skill
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -453,7 +453,7 @@ export default function SkillsPanel({
|
|||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
{/* loading 时不显示条数 —— 旧数据已清空,显示"0 条"会误导 */}
|
{/* loading 时不显示条数 —— 旧数据已清空,显示"0 条"会误导 */}
|
||||||
{!loading && <Text theme="weak">{skillsWithCache.length} 条</Text>}
|
{!loading && <Text theme="weak">{skillsWithCache.length} items</Text>}
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="_memory-skills-list-items">
|
<div className="_memory-skills-list-items">
|
||||||
@ -469,10 +469,10 @@ export default function SkillsPanel({
|
|||||||
<div className="_memory-skills-list-empty">
|
<div className="_memory-skills-list-empty">
|
||||||
<Text theme="weak">
|
<Text theme="weak">
|
||||||
{tab === 'fixed' && !selectedAgent
|
{tab === 'fixed' && !selectedAgent
|
||||||
? '请选择一个 agent。'
|
? 'Select an agent.'
|
||||||
: tab === 'fixed'
|
: tab === 'fixed'
|
||||||
? `Agent "${selectedAgent}" 暂无固定 skill。点击右上「导入 Skill」直接导入,或在「团队」视图选中一条 skill 后通过「Fork」分发。`
|
? `Agent "${selectedAgent}" has no fixed skills yet. Click "Import skill" in the top right to import one directly, or select a skill in the "Team" view and distribute it via "Fork".`
|
||||||
: '团队里还没有任何"共享"skill。skill 新建时默认私密,只有 owner 自己能看到;如需让整个团队看到,需要 owner 在「我的资产分配」tab 里点共享按钮。'}
|
: 'The team has no "shared" skills yet. New skills default to private and are visible only to their owner. To make one visible to the whole team, the owner needs to click the share button in the "My asset allocations" tab.'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -500,13 +500,13 @@ export default function SkillsPanel({
|
|||||||
{/* 可见性徽章:从 visibilityMap 里读;用 Tea Tag 渲染(与 TaskWorkbench 一致) */}
|
{/* 可见性徽章:从 visibilityMap 里读;用 Tea Tag 渲染(与 TaskWorkbench 一致) */}
|
||||||
{vis === 'private' && (
|
{vis === 'private' && (
|
||||||
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-state-tag">
|
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-state-tag">
|
||||||
<span className="_memory-skill-tag-content"><LockOnIcon size={10} /> 私密</span>
|
<span className="_memory-skill-tag-content"><LockOnIcon size={10} /> Private</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{/* 团队 tab 的全部条目都已是共享资产,不重复占用标题行空间。 */}
|
{/* 团队 tab 的全部条目都已是共享资产,不重复占用标题行空间。 */}
|
||||||
{vis === 'team' && tab !== 'team' && (
|
{vis === 'team' && tab !== 'team' && (
|
||||||
<Tag theme="success" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-state-tag">
|
<Tag theme="success" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-state-tag">
|
||||||
<span className="_memory-skill-tag-content"><ShareIcon size={10} /> 共享</span>
|
<span className="_memory-skill-tag-content"><ShareIcon size={10} /> Shared</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{vis && vis !== 'private' && vis !== 'team' && (
|
{vis && vis !== 'private' && vis !== 'team' && (
|
||||||
@ -514,7 +514,7 @@ export default function SkillsPanel({
|
|||||||
)}
|
)}
|
||||||
{/* owner agent 徽章:自己 owner 的高亮(warning),别人的用 default */}
|
{/* owner agent 徽章:自己 owner 的高亮(warning),别人的用 default */}
|
||||||
{s.owner_agent_id && (
|
{s.owner_agent_id && (
|
||||||
<span title={`owner agent: ${agentNameMap[s.owner_agent_id] ?? '(未知)'}(${s.owner_agent_id})`}>
|
<span title={`owner agent: ${agentNameMap[s.owner_agent_id] ?? '(unknown)'} (${s.owner_agent_id})`}>
|
||||||
<Tag
|
<Tag
|
||||||
theme={ownerIsMe ? 'warning' : 'default'}
|
theme={ownerIsMe ? 'warning' : 'default'}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@ -535,7 +535,7 @@ export default function SkillsPanel({
|
|||||||
<Button
|
<Button
|
||||||
type="icon"
|
type="icon"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
tooltip={ownerIsMe ? '彻底删除我的 Skill(不可恢复)' : '以管理员身份彻底删除此 Skill(不可恢复)'}
|
tooltip={ownerIsMe ? 'Permanently delete my skill (cannot be undone)' : 'Permanently delete this skill as admin (cannot be undone)'}
|
||||||
className="_memory-skill-item-delete"
|
className="_memory-skill-item-delete"
|
||||||
onClick={async (e: any) => {
|
onClick={async (e: any) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
@ -544,13 +544,13 @@ export default function SkillsPanel({
|
|||||||
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent,
|
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent,
|
||||||
// 其他 Agent 下同名的独立副本不受影响。按"彻底删除"语义描述。
|
// 其他 Agent 下同名的独立副本不受影响。按"彻底删除"语义描述。
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认彻底删除 Skill「${s.name}」?`,
|
message: `Permanently delete skill "${s.name}"?`,
|
||||||
description:
|
description:
|
||||||
'删除后该 Skill 将从所属 Agent 卸载,且不可恢复。' +
|
'Deleting it will uninstall the skill from its agent and cannot be undone. ' +
|
||||||
'其他 Agent 下同名的独立副本不受影响。' +
|
'Independent copies with the same name under other agents are not affected. ' +
|
||||||
'如仅需临时停用,请考虑将其设为"私密"而非删除。',
|
'If you only need to disable it temporarily, consider setting it to "Private" instead of deleting it.',
|
||||||
okText: '彻底删除',
|
okText: 'Delete permanently',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
void handleDelete(s);
|
void handleDelete(s);
|
||||||
@ -755,13 +755,13 @@ function PersonalAssetTab({
|
|||||||
// Skill 数据面为软归档;meta asset 经钩子物理删除。前端按"彻底删除"描述。
|
// Skill 数据面为软归档;meta asset 经钩子物理删除。前端按"彻底删除"描述。
|
||||||
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent,其他 Agent 的独立副本不受影响。
|
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent,其他 Agent 的独立副本不受影响。
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认彻底删除 Skill「${asset.name}」?`,
|
message: `Permanently delete skill "${asset.name}"?`,
|
||||||
description:
|
description:
|
||||||
'删除后该 Skill 将从所属 Agent 卸载,且不可恢复。' +
|
'Deleting it will uninstall the skill from its agent and cannot be undone. ' +
|
||||||
'其他 Agent 下同名的独立副本不受影响。' +
|
'Independent copies with the same name under other agents are not affected. ' +
|
||||||
'如仅需临时停用,请考虑将其设为"私密"而非删除。',
|
'If you only need to disable it temporarily, consider setting it to "Private" instead of deleting it.',
|
||||||
okText: '彻底删除',
|
okText: 'Delete permanently',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
setBusyId(asset.asset_id);
|
setBusyId(asset.asset_id);
|
||||||
@ -800,7 +800,7 @@ function PersonalAssetTab({
|
|||||||
}
|
}
|
||||||
setAssets((prev) => prev.filter((a) => a.asset_id !== asset.asset_id));
|
setAssets((prev) => prev.filter((a) => a.asset_id !== asset.asset_id));
|
||||||
if (selectedAssetId === asset.asset_id) onSelectAsset?.(null);
|
if (selectedAssetId === asset.asset_id) onSelectAsset?.(null);
|
||||||
tea.notify.success(`已删除 Skill「${asset.name}」`);
|
tea.notify.success(`Deleted skill "${asset.name}"`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tea.notify.error(err);
|
tea.notify.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -813,20 +813,20 @@ function PersonalAssetTab({
|
|||||||
<Card.Body>
|
<Card.Body>
|
||||||
{/* 顶部 */}
|
{/* 顶部 */}
|
||||||
<div className="_memory-personal-header">
|
<div className="_memory-personal-header">
|
||||||
<Text theme="strong" parent="div">我的资产分配</Text>
|
<Text theme="strong" parent="div">My asset allocations</Text>
|
||||||
<Text theme="weak" parent="div" className="_memory-personal-header-desc">
|
<Text theme="weak" parent="div" className="_memory-personal-header-desc">
|
||||||
仅显示你是 owner 的资产 · 切换「共享 / 私密」决定其他 team 成员能否看到
|
Shows only assets you own · toggle "Shared / Private" to control whether other team members can see it
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="_memory-personal-empty">
|
<div className="_memory-personal-empty">
|
||||||
<Text theme="weak">加载中…</Text>
|
<Text theme="weak">Loading...</Text>
|
||||||
</div>
|
</div>
|
||||||
) : assets.length === 0 ? (
|
) : assets.length === 0 ? (
|
||||||
<div className="_memory-personal-empty">
|
<div className="_memory-personal-empty">
|
||||||
<Text theme="weak" parent="div">
|
<Text theme="weak" parent="div">
|
||||||
暂无你 owner 的{kind === 'skill' ? '技能' : '记忆'}资产 · 通过右上角「导入 Skill」新建
|
You don't own any {kind === 'skill' ? 'skill' : 'memory'} assets yet · create one via "Import skill" in the top right
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -859,16 +859,16 @@ function PersonalAssetTab({
|
|||||||
<div className="_memory-personal-asset-badges">
|
<div className="_memory-personal-asset-badges">
|
||||||
{cachedAgentId && (
|
{cachedAgentId && (
|
||||||
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
<Tag theme="default" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
||||||
<span className="_memory-skill-tag-content" title={`owner agent: ${agentNameMap[cachedAgentId] ?? '(未知)'}(${cachedAgentId})`}>
|
<span className="_memory-skill-tag-content" title={`owner agent: ${agentNameMap[cachedAgentId] ?? '(unknown)'} (${cachedAgentId})`}>
|
||||||
<AppIcon size={10} /> {agentNameMap[cachedAgentId] ?? cachedAgentId}
|
<AppIcon size={10} /> {agentNameMap[cachedAgentId] ?? cachedAgentId}
|
||||||
</span>
|
</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{asset.owner_user_id && (
|
{asset.owner_user_id && (
|
||||||
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
<Tag theme="primary" variant="soft" size="sm" shapeType="rectangle" className="_memory-skill-owner-tag">
|
||||||
<span className="_memory-skill-tag-content" title={`owner user: ${currentUserName || asset.owner_user_id}(${asset.owner_user_id})`}>
|
<span className="_memory-skill-tag-content" title={`owner user: ${currentUserName || asset.owner_user_id} (${asset.owner_user_id})`}>
|
||||||
<UserIcon size={10} /> {currentUserName || asset.owner_user_id}
|
<UserIcon size={10} /> {currentUserName || asset.owner_user_id}
|
||||||
{asset.owner_user_id === currentUser && '(你)'}
|
{asset.owner_user_id === currentUser && ' (you)'}
|
||||||
</span>
|
</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
@ -888,24 +888,24 @@ function PersonalAssetTab({
|
|||||||
type={isTeam ? 'primary' : 'weak'}
|
type={isTeam ? 'primary' : 'weak'}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onClick={() => void handleSetScope(asset, 'team')}
|
onClick={() => void handleSetScope(asset, 'team')}
|
||||||
tooltip="team 内成员可读;owner 和 admin 可写"
|
tooltip="Readable by team members; writable by owner and admin"
|
||||||
>
|
>
|
||||||
<ShareIcon size={12} /> 共享
|
<ShareIcon size={12} /> Shared
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type={isPrivate ? 'primary' : 'weak'}
|
type={isPrivate ? 'primary' : 'weak'}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onClick={() => void handleSetScope(asset, 'private')}
|
onClick={() => void handleSetScope(asset, 'private')}
|
||||||
tooltip="只有 owner 和 team admin 能看到"
|
tooltip="Visible only to the owner and team admin"
|
||||||
>
|
>
|
||||||
<LockOnIcon size={12} /> 私密
|
<LockOnIcon size={12} /> Private
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 删除:Tea 图标按钮(error 主题),二次确认在 handleDelete 内 */}
|
{/* 删除:Tea 图标按钮(error 主题),二次确认在 handleDelete 内 */}
|
||||||
<Button type="text"
|
<Button type="text"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
tooltip="彻底删除该 Skill(不可恢复)"
|
tooltip="Permanently delete this skill (cannot be undone)"
|
||||||
className="_memory-personal-asset-delete"
|
className="_memory-personal-asset-delete"
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
|
|||||||
@ -113,9 +113,9 @@ export default function ApiKeyPanel() {
|
|||||||
|
|
||||||
async function handleDelete(key: UserKey) {
|
async function handleDelete(key: UserKey) {
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认吊销 Key「${key.key_prefix || key.key_id}」?`,
|
message: `Revoke key "${key.key_prefix || key.key_id}"?`,
|
||||||
description: '吊销后对应客户端将立即失效,且不可恢复。',
|
description: 'Once revoked, the corresponding client will lose access immediately, and this cannot be undone.',
|
||||||
okText: '吊销',
|
okText: 'Revoke',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
@ -140,8 +140,8 @@ export default function ApiKeyPanel() {
|
|||||||
<Alert type="success" onClose={() => setFreshKey(null)}>
|
<Alert type="success" onClose={() => setFreshKey(null)}>
|
||||||
<div className="_memory-apikey-fresh">
|
<div className="_memory-apikey-fresh">
|
||||||
<p className="_memory-apikey-fresh-desc">
|
<p className="_memory-apikey-fresh-desc">
|
||||||
以下是 <strong>{freshKey.keyId}</strong> 的完整 Key(<strong>仅展示这一次</strong>
|
Below is the full key for <strong>{freshKey.keyId}</strong> (<strong>shown only this once</strong>
|
||||||
,请立即复制并安全保存;关闭后将无法再次查看明文):
|
, copy and store it securely now; once closed, the plaintext cannot be viewed again):
|
||||||
</p>
|
</p>
|
||||||
<div className="_memory-apikey-fresh-code-row">
|
<div className="_memory-apikey-fresh-code-row">
|
||||||
<code className="_memory-apikey-fresh-code">{freshKey.secret}</code>
|
<code className="_memory-apikey-fresh-code">{freshKey.secret}</code>
|
||||||
@ -161,9 +161,9 @@ export default function ApiKeyPanel() {
|
|||||||
<Justify
|
<Justify
|
||||||
left={
|
left={
|
||||||
<div>
|
<div>
|
||||||
<H3>User_Key 管理</H3>
|
<H3>User_Key management</H3>
|
||||||
<Text theme="text" parent="div" style={{ marginTop: 4 }}>
|
<Text theme="text" parent="div" style={{ marginTop: 4 }}>
|
||||||
管理你的 User Key,用于外部客户端接入(如 CodeBuddy / ClaudeCode CLI)。
|
Manage your User Keys, used for external client access (e.g. CodeBuddy / Claude Code CLI).
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ export default function ApiKeyPanel() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddIcon size={14} />
|
<AddIcon size={14} />
|
||||||
新建 Key
|
Create key
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@ -209,26 +209,26 @@ export default function ApiKeyPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
header: '创建时间',
|
header: 'Created at',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (key) => <Text theme="text">{formatTime(key.created_at)}</Text>,
|
render: (key) => <Text theme="text">{formatTime(key.created_at)}</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'expires_at',
|
key: 'expires_at',
|
||||||
header: '失效时间',
|
header: 'Expires at',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (key) => {
|
render: (key) => {
|
||||||
if (key.revoked_at) return <Text theme="weak">已吊销</Text>;
|
if (key.revoked_at) return <Text theme="weak">Revoked</Text>;
|
||||||
return key.expires_at ? (
|
return key.expires_at ? (
|
||||||
<Text theme="text">{formatTime(key.expires_at)}</Text>
|
<Text theme="text">{formatTime(key.expires_at)}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text theme="weak">永不过期</Text>
|
<Text theme="weak">Never expires</Text>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '操作',
|
header: 'Actions',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (key) => (
|
render: (key) => (
|
||||||
@ -236,7 +236,7 @@ export default function ApiKeyPanel() {
|
|||||||
disabled={!!key.revoked_at}
|
disabled={!!key.revoked_at}
|
||||||
onClick={() => void handleDelete(key)}
|
onClick={() => void handleDelete(key)}
|
||||||
>
|
>
|
||||||
吊销
|
Revoke
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -246,9 +246,9 @@ export default function ApiKeyPanel() {
|
|||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="_memory-apikey-empty">
|
<div className="_memory-apikey-empty">
|
||||||
<div className="_memory-apikey-empty-title">你还没有任何 User Key</div>
|
<div className="_memory-apikey-empty-title">You don't have any User Keys yet</div>
|
||||||
<div className="_memory-apikey-empty-desc">
|
<div className="_memory-apikey-empty-desc">
|
||||||
点击右上角「新建 Key」创建你的第一把 Key
|
Click "Create key" in the top-right to create your first key
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -265,10 +265,10 @@ export default function ApiKeyPanel() {
|
|||||||
未登录理论上不会走到这个页(LoginGate 挡在外面),仍保留占位 fallback 兜底。
|
未登录理论上不会走到这个页(LoginGate 挡在外面),仍保留占位 fallback 兜底。
|
||||||
*/}
|
*/}
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Body title="客户端接入地址">
|
<Card.Body title="Client access address">
|
||||||
{auth?.instance_name && (
|
{auth?.instance_name && (
|
||||||
<div style={{ marginBottom: 8, fontSize: 11, color: 'var(--text-weak)' }}>
|
<div style={{ marginBottom: 8, fontSize: 11, color: 'var(--text-weak)' }}>
|
||||||
当前实例:<code>{auth.instance_name}</code>
|
Current instance: <code>{auth.instance_name}</code>
|
||||||
<span style={{ opacity: 0.6, marginLeft: 6 }}>({auth.instance_id})</span>
|
<span style={{ opacity: 0.6, marginLeft: 6 }}>({auth.instance_id})</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -278,7 +278,7 @@ export default function ApiKeyPanel() {
|
|||||||
if (!gatewayEndpoint) {
|
if (!gatewayEndpoint) {
|
||||||
return (
|
return (
|
||||||
<Text theme="weak" style={{ fontSize: 11 }}>
|
<Text theme="weak" style={{ fontSize: 11 }}>
|
||||||
正在加载接入地址…
|
Loading access address…
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -308,7 +308,7 @@ export default function ApiKeyPanel() {
|
|||||||
{ep.url}
|
{ep.url}
|
||||||
</code>
|
</code>
|
||||||
<Copy text={ep.url}>
|
<Copy text={ep.url}>
|
||||||
<Button>复制</Button>
|
<Button>Copy</Button>
|
||||||
</Copy>
|
</Copy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -319,22 +319,22 @@ export default function ApiKeyPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
{/* ===== 新建弹窗:只需设置「过期时间」(可留空=永不过期),不再需要名称 ===== */}
|
{/* ===== 新建弹窗:只需设置「过期时间」(可留空=永不过期),不再需要名称 ===== */}
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<Modal visible caption="新建 User_Key" size="s" onClose={() => setShowCreate(false)} disableEscape={creating}>
|
<Modal visible caption="Create User_Key" size="s" onClose={() => setShowCreate(false)} disableEscape={creating}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="过期时间" extra="留空表示永不过期">
|
<Form.Item label="Expiration" extra="Leave blank for never expires">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={newExpiresAt ?? undefined}
|
value={newExpiresAt ?? undefined}
|
||||||
onChange={(v) => setNewExpiresAt(v)}
|
onChange={(v) => setNewExpiresAt(v)}
|
||||||
disabledDate={(d) => !d.isBefore(moment().startOf('day'))}
|
disabledDate={(d) => !d.isBefore(moment().startOf('day'))}
|
||||||
placeholder="留空表示永不过期"
|
placeholder="Leave blank for never expires"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void handleCreate()} disabled={creating} loading={creating}>创建</Button>
|
<Button type="primary" onClick={() => void handleCreate()} disabled={creating} loading={creating}>Create</Button>
|
||||||
<Button onClick={() => setShowCreate(false)} disabled={creating}>取消</Button>
|
<Button onClick={() => setShowCreate(false)} disabled={creating}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export default function AgentEditDialog({
|
|||||||
const nextRolePrompt = rolePrompt.trim();
|
const nextRolePrompt = rolePrompt.trim();
|
||||||
const nextRulesPrompt = rulesPrompt.trim();
|
const nextRulesPrompt = rulesPrompt.trim();
|
||||||
if (!nextName) {
|
if (!nextName) {
|
||||||
tea.notify.error('Agent 名称不能为空。');
|
tea.notify.error('Agent name cannot be empty.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSavingPrompt(true);
|
setSavingPrompt(true);
|
||||||
@ -149,10 +149,10 @@ export default function AgentEditDialog({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
invalidateBackendCache();
|
invalidateBackendCache();
|
||||||
tea.notify.success('Agent 信息已保存。');
|
tea.notify.success('Agent information saved.');
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tea.notify.error(`保存 Agent 信息失败:${error instanceof Error ? error.message : String(error)}`);
|
tea.notify.error(`Failed to save agent information: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
} finally {
|
} finally {
|
||||||
setSavingPrompt(false);
|
setSavingPrompt(false);
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ export default function AgentEditDialog({
|
|||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
// 只读模式下后端可能拒绝访问非自己的 agent 资产(NOT_YOUR_AGENT),这是预期行为。
|
// 只读模式下后端可能拒绝访问非自己的 agent 资产(NOT_YOUR_AGENT),这是预期行为。
|
||||||
if (!/NOT_YOUR_AGENT/.test(msg)) {
|
if (!/NOT_YOUR_AGENT/.test(msg)) {
|
||||||
tea.notify.error(`加载 Agent 资产绑定失败:${msg}`);
|
tea.notify.error(`Failed to load agent asset bindings: ${msg}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,30 +207,30 @@ export default function AgentEditDialog({
|
|||||||
}, [agent, assets.loading, realBindingsLoaded, selfChatMemoryId]);
|
}, [agent, assets.loading, realBindingsLoaded, selfChatMemoryId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="Agent 详情" size="l" onClose={onClose}>
|
<Modal visible caption="Agent details" size="l" onClose={onClose}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div className="_memory-form-stack">
|
<div className="_memory-form-stack">
|
||||||
<div className="_memory-modal-description">{agent.agent_id}</div>
|
<div className="_memory-modal-description">{agent.agent_id}</div>
|
||||||
<LightField label="名称">
|
<LightField label="Name">
|
||||||
<Input size="full" value={name} onChange={setName} disabled={savingPrompt} />
|
<Input size="full" value={name} onChange={setName} disabled={savingPrompt} />
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField label="一句话描述">
|
<LightField label="One-line description">
|
||||||
<Input.TextArea size="full" value={description} onChange={setDescription} rows={2} disabled={savingPrompt} />
|
<Input.TextArea size="full" value={description} onChange={setDescription} rows={2} disabled={savingPrompt} />
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField label="角色定位 prompt">
|
<LightField label="Role prompt">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={rolePrompt}
|
value={rolePrompt}
|
||||||
onChange={setRolePrompt}
|
onChange={setRolePrompt}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={savingPrompt}
|
disabled={savingPrompt}
|
||||||
placeholder="描述这个 agent 扮演什么角色 / 职责定位..."
|
placeholder="Describe what role/responsibility this agent plays..."
|
||||||
/>
|
/>
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField label="规则固定 prompt">
|
<LightField label="Rules prompt">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={rulesPrompt}
|
value={rulesPrompt}
|
||||||
@ -238,23 +238,23 @@ export default function AgentEditDialog({
|
|||||||
rows={4}
|
rows={4}
|
||||||
disabled={savingPrompt}
|
disabled={savingPrompt}
|
||||||
className="_memory-mono-textarea"
|
className="_memory-mono-textarea"
|
||||||
placeholder="为 Agent 设定行为规则提示词..."
|
placeholder="Set behavior rule prompts for the agent..."
|
||||||
/>
|
/>
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<div className="_memory-asset-section">
|
<div className="_memory-asset-section">
|
||||||
{assets.loading ? (
|
{assets.loading ? (
|
||||||
<div className="_memory-asset-loading">加载团队资产中…</div>
|
<div className="_memory-asset-loading">Loading team assets…</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="_memory-asset-toolbar">
|
<div className="_memory-asset-toolbar">
|
||||||
<span className="_memory-asset-toolbar-label">原子能力</span>
|
<span className="_memory-asset-toolbar-label">Atomic capabilities</span>
|
||||||
<span className="_memory-asset-toolbar-hint">只读 · 资源绑定请在创建或者对应资源管理页面修改设置</span>
|
<span className="_memory-asset-toolbar-hint">Read-only · to change resource bindings, edit them on creation or in the corresponding resource management page</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-collapse-group-stack">
|
<div className="_memory-collapse-group-stack">
|
||||||
<CollapseGroup
|
<CollapseGroup
|
||||||
icon={<BooksIcon size={16} />}
|
icon={<BooksIcon size={16} />}
|
||||||
title="Wiki 知识库"
|
title="Wiki knowledge base"
|
||||||
selectedCount={llmWikis.length}
|
selectedCount={llmWikis.length}
|
||||||
totalCount={boundWikis.length}
|
totalCount={boundWikis.length}
|
||||||
open={wikiOpen}
|
open={wikiOpen}
|
||||||
@ -286,7 +286,7 @@ export default function AgentEditDialog({
|
|||||||
</CollapseGroup>
|
</CollapseGroup>
|
||||||
<CollapseGroup
|
<CollapseGroup
|
||||||
icon={<ToolsIcon size={16} />}
|
icon={<ToolsIcon size={16} />}
|
||||||
title="Skill 技能"
|
title="Skill"
|
||||||
selectedCount={skills.length}
|
selectedCount={skills.length}
|
||||||
totalCount={boundSkills.length}
|
totalCount={boundSkills.length}
|
||||||
open={skillsOpen}
|
open={skillsOpen}
|
||||||
@ -323,9 +323,9 @@ export default function AgentEditDialog({
|
|||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button onClick={onClose} disabled={savingPrompt}>取消</Button>
|
<Button onClick={onClose} disabled={savingPrompt}>Cancel</Button>
|
||||||
<Button type="primary" onClick={() => void saveAgent()} disabled={!agentChanged || savingPrompt} loading={savingPrompt}>
|
<Button type="primary" onClick={() => void saveAgent()} disabled={!agentChanged || savingPrompt} loading={savingPrompt}>
|
||||||
保存修改
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export default function AgentGrid({
|
|||||||
className={`_memory-agents-name-trigger${editable ? ' _memory-agents-name-trigger--editable' : ''}`}
|
className={`_memory-agents-name-trigger${editable ? ' _memory-agents-name-trigger--editable' : ''}`}
|
||||||
onClick={() => editable && onEditAgent(agent)}
|
onClick={() => editable && onEditAgent(agent)}
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
title={editable ? '点击查看并编辑该 Agent' : `仅 owner(${agent.owner_user_id || '未设置'})或 team 管理员可编辑`}
|
title={editable ? 'Click to view and edit this agent' : `Only the owner (${agent.owner_user_id || 'not set'}) or a team admin can edit`}
|
||||||
>
|
>
|
||||||
<span className={`_memory-agents-icon ${acc.bg}`}>{agent.icon}</span>
|
<span className={`_memory-agents-icon ${acc.bg}`}>{agent.icon}</span>
|
||||||
<span className="_memory-agents-name" title={agent.name}>{agent.name}</span>
|
<span className="_memory-agents-name" title={agent.name}>{agent.name}</span>
|
||||||
@ -108,7 +108,7 @@ export default function AgentGrid({
|
|||||||
const ownerIsMe = agent.owner_user_id === currentUser;
|
const ownerIsMe = agent.owner_user_id === currentUser;
|
||||||
return (
|
return (
|
||||||
<Tag theme={ownerIsMe ? 'warning' : 'default'} size="sm">
|
<Tag theme={ownerIsMe ? 'warning' : 'default'} size="sm">
|
||||||
{agent.owner_user_id || '未设置'}{ownerIsMe && '(你)'}
|
{agent.owner_user_id || 'not set'}{ownerIsMe && ' (you)'}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -131,9 +131,9 @@ export default function AgentGrid({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="_memory-agents-section-title">Agents</h2>
|
<h2 className="_memory-agents-section-title">Agents</h2>
|
||||||
<div className="_memory-agents-section-subtitle">
|
<div className="_memory-agents-section-subtitle">
|
||||||
当前 team「{activeTeam.name}」
|
Agents you created in team "{activeTeam.name}"
|
||||||
<span className="_memory-mono-inline">({activeTeam.team_id})</span>
|
<span className="_memory-mono-inline"> ({activeTeam.team_id})</span>
|
||||||
中由你创建的 Agent · {agentsLoading ? '加载中…' : `共 ${agents.length} 个`}
|
· {agentsLoading ? 'Loading…' : `${agents.length} total`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -145,9 +145,9 @@ export default function AgentGrid({
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={onCreateAgent}
|
onClick={onCreateAgent}
|
||||||
style={{ visibility: isAdmin ? 'hidden' : 'visible' }}
|
style={{ visibility: isAdmin ? 'hidden' : 'visible' }}
|
||||||
title="在当前 team 下创建一个新 Agent"
|
title="Create a new agent in the current team"
|
||||||
>
|
>
|
||||||
<AddIcon size={12} /> 新建 Agent
|
<AddIcon size={12} /> Create agent
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
@ -155,7 +155,7 @@ export default function AgentGrid({
|
|||||||
<SearchBox
|
<SearchBox
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={setKeyword}
|
onChange={setKeyword}
|
||||||
placeholder="搜索 Agent 名称 / 描述 / ID"
|
placeholder="Search by agent name / description / ID"
|
||||||
/>
|
/>
|
||||||
{canSeeAllAgents && (
|
{canSeeAllAgents && (
|
||||||
<Select
|
<Select
|
||||||
@ -163,7 +163,7 @@ export default function AgentGrid({
|
|||||||
onChange={setOwnerFilter}
|
onChange={setOwnerFilter}
|
||||||
appearance="button"
|
appearance="button"
|
||||||
options={[
|
options={[
|
||||||
{ value: '', text: '全部 Owner' },
|
{ value: '', text: 'All owners' },
|
||||||
...ownerOptions.map((ownerId) => ({ value: ownerId, text: ownerId })),
|
...ownerOptions.map((ownerId) => ({ value: ownerId, text: ownerId })),
|
||||||
]}
|
]}
|
||||||
matchButtonWidth
|
matchButtonWidth
|
||||||
@ -183,16 +183,16 @@ export default function AgentGrid({
|
|||||||
</Table.ActionPanel>
|
</Table.ActionPanel>
|
||||||
|
|
||||||
{agentsLoading && agents.length === 0 ? (
|
{agentsLoading && agents.length === 0 ? (
|
||||||
<div className="_memory-agents-empty">正在加载 Agent…</div>
|
<div className="_memory-agents-empty">Loading agents…</div>
|
||||||
) : filteredAgents.length === 0 ? (
|
) : filteredAgents.length === 0 ? (
|
||||||
<div className="_memory-agents-empty">
|
<div className="_memory-agents-empty">
|
||||||
{agents.length === 0
|
{agents.length === 0
|
||||||
? isAdmin
|
? isAdmin
|
||||||
? '当前 team 下还没有 Agent'
|
? 'No agents in the current team yet'
|
||||||
: '还没有 Agent · 点击左上角「+ 新建 Agent」创建第一个'
|
: 'No agents yet · click "+ Create agent" in the top-left to create your first one'
|
||||||
: canSeeAllAgents
|
: canSeeAllAgents
|
||||||
? '没有符合搜索或 Owner 筛选条件的 Agent'
|
? 'No agents match the search or owner filter'
|
||||||
: '没有符合搜索条件的 Agent'}
|
: 'No agents match the search'}
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'card' ? (
|
) : viewMode === 'card' ? (
|
||||||
<div className="_memory-agents-card-grid">
|
<div className="_memory-agents-card-grid">
|
||||||
@ -202,11 +202,11 @@ export default function AgentGrid({
|
|||||||
<div key={agent.agent_id} className={`_memory-agents-card${editable ? ' _memory-agents-card--editable' : ''}`}>
|
<div key={agent.agent_id} className={`_memory-agents-card${editable ? ' _memory-agents-card--editable' : ''}`}>
|
||||||
<div className="_memory-agents-card-head">{renderName(agent)}</div>
|
<div className="_memory-agents-card-head">{renderName(agent)}</div>
|
||||||
<div className="_memory-agents-card-id">id: {agent.agent_id}</div>
|
<div className="_memory-agents-card-id">id: {agent.agent_id}</div>
|
||||||
<div className="_memory-agents-card-desc">{agent.description || '暂无描述'}</div>
|
<div className="_memory-agents-card-desc">{agent.description || 'No description'}</div>
|
||||||
<div className="_memory-agents-owner-row">
|
<div className="_memory-agents-owner-row">
|
||||||
<span>owner</span>
|
<span>owner</span>
|
||||||
{renderOwner(agent)}
|
{renderOwner(agent)}
|
||||||
{!editable && <span className="_memory-agents-readonly">· 只读</span>}
|
{!editable && <span className="_memory-agents-readonly">· read-only</span>}
|
||||||
</div>
|
</div>
|
||||||
{renderAssets(agent)}
|
{renderAssets(agent)}
|
||||||
<div className="_memory-agents-card-actions">
|
<div className="_memory-agents-card-actions">
|
||||||
@ -214,9 +214,9 @@ export default function AgentGrid({
|
|||||||
type="text"
|
type="text"
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
onClick={() => onDeleteAgent(agent)}
|
onClick={() => onDeleteAgent(agent)}
|
||||||
title={editable ? '删除该 Agent' : '你没有删除该 Agent 的权限'}
|
title={editable ? 'Delete this agent' : 'You do not have permission to delete this agent'}
|
||||||
>
|
>
|
||||||
<DeleteIcon size={12} /> 删除
|
<DeleteIcon size={12} /> Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,7 +231,7 @@ export default function AgentGrid({
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
header: '名称',
|
header: 'Name',
|
||||||
width: 240,
|
width: 240,
|
||||||
render: (agent: StoreAgent) => renderName(agent, true),
|
render: (agent: StoreAgent) => renderName(agent, true),
|
||||||
},
|
},
|
||||||
@ -243,7 +243,7 @@ export default function AgentGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'assets',
|
key: 'assets',
|
||||||
header: '挂载资产',
|
header: 'Mounted assets',
|
||||||
render: (agent: StoreAgent) => {
|
render: (agent: StoreAgent) => {
|
||||||
const counts = mountedCounts[agent.agent_id] ?? emptyMountedCounts();
|
const counts = mountedCounts[agent.agent_id] ?? emptyMountedCounts();
|
||||||
return (
|
return (
|
||||||
@ -255,19 +255,19 @@ export default function AgentGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'description',
|
key: 'description',
|
||||||
header: '描述',
|
header: 'Description',
|
||||||
render: (agent: StoreAgent) => <span className="_memory-agents-list-description">{agent.description || '暂无描述'}</span>,
|
render: (agent: StoreAgent) => <span className="_memory-agents-list-description">{agent.description || 'No description'}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '操作',
|
header: 'Actions',
|
||||||
width: 90,
|
width: 90,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (agent: StoreAgent) => {
|
render: (agent: StoreAgent) => {
|
||||||
const editable = canEdit(agent);
|
const editable = canEdit(agent);
|
||||||
return (
|
return (
|
||||||
<Button type="link" disabled={!editable} onClick={() => onDeleteAgent(agent)}>
|
<Button type="link" disabled={!editable} onClick={() => onDeleteAgent(agent)}>
|
||||||
删除
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -128,31 +128,31 @@ export default function CreateAgentDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="创建 Agent" size="l" onClose={onClose} disableEscape={busy}>
|
<Modal visible caption="Create agent" size="l" onClose={onClose} disableEscape={busy}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div className="_memory-form-stack">
|
<div className="_memory-form-stack">
|
||||||
<div className="_memory-modal-description">只有名字必填 · 描述 / 规则 prompt / 原子能力都可留空,创建后再补</div>
|
<div className="_memory-modal-description">Only the name is required · description / role & rules prompts / atomic capabilities can be left blank and filled in later</div>
|
||||||
<div className="_memory-target-team-row">
|
<div className="_memory-target-team-row">
|
||||||
<span className="_memory-target-team-avatar">{team.name.slice(0, 1).toUpperCase()}</span>
|
<span className="_memory-target-team-avatar">{team.name.slice(0, 1).toUpperCase()}</span>
|
||||||
<div className="_memory-target-team-meta">
|
<div className="_memory-target-team-meta">
|
||||||
<div className="_memory-target-team-label">将创建到 team</div>
|
<div className="_memory-target-team-label">Will be created in team</div>
|
||||||
<div className="_memory-target-team-name-row">
|
<div className="_memory-target-team-name-row">
|
||||||
<span className="_memory-target-team-name">{team.name}</span>
|
<span className="_memory-target-team-name">{team.name}</span>
|
||||||
<Tag size="sm">{team.team_id}</Tag>
|
<Tag size="sm">{team.team_id}</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-target-team-hint">
|
<div className="_memory-target-team-hint">
|
||||||
切 team 请到
|
To switch team, go to
|
||||||
<br />
|
<br />
|
||||||
左上角
|
the top-left corner
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="_memory-template-box">
|
<div className="_memory-template-box">
|
||||||
<div className="_memory-template-box-title-row">
|
<div className="_memory-template-box-title-row">
|
||||||
<span className="_memory-template-box-title">套用模板</span>
|
<span className="_memory-template-box-title">Apply template</span>
|
||||||
<span className="_memory-template-box-hint">
|
<span className="_memory-template-box-hint">
|
||||||
选填 · 一键预填描述 / prompt / 原子能力,名字仍需自己填
|
Optional · one click prefills description / prompts / atomic capabilities, name still required
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-template-chip-row">
|
<div className="_memory-template-chip-row">
|
||||||
@ -175,9 +175,9 @@ export default function CreateAgentDialog({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDeleteTemplate(tpl)}
|
onClick={() => handleDeleteTemplate(tpl)}
|
||||||
title="删除该自定义模板"
|
title="Delete this custom template"
|
||||||
className="_memory-template-chip-close"
|
className="_memory-template-chip-close"
|
||||||
aria-label="删除该自定义模板"
|
aria-label="Delete this custom template"
|
||||||
>
|
>
|
||||||
<CloseIcon size={10} />
|
<CloseIcon size={10} />
|
||||||
</button>
|
</button>
|
||||||
@ -190,102 +190,102 @@ export default function CreateAgentDialog({
|
|||||||
onClick={openSaveTemplateForm}
|
onClick={openSaveTemplateForm}
|
||||||
className="_memory-template-save-btn"
|
className="_memory-template-save-btn"
|
||||||
>
|
>
|
||||||
<AddIcon size={11} /> 保存为模板
|
<AddIcon size={11} /> Save as template
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{saveTplOpen && (
|
{saveTplOpen && (
|
||||||
<div className="_memory-template-save-form">
|
<div className="_memory-template-save-form">
|
||||||
<div className="_memory-template-save-hint">
|
<div className="_memory-template-save-hint">
|
||||||
把当前表单存成可复用的自定义模板(数据存储在浏览器本地,清除缓存会丢失已保存的模板)。以下内容默认取自当前表单,可在此微调。
|
Save the current form as a reusable custom template (stored locally in the browser; clearing cache will lose saved templates). The fields below default to the current form values and can be tweaked here.
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={tplName}
|
value={tplName}
|
||||||
onChange={setTplName}
|
onChange={setTplName}
|
||||||
placeholder="模板名(必填),如:安全审计 Reviewer"
|
placeholder="Template name (required), e.g. Security audit reviewer"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={tplSummary}
|
value={tplSummary}
|
||||||
onChange={setTplSummary}
|
onChange={setTplSummary}
|
||||||
placeholder="一句话说明(选填)"
|
placeholder="One-line summary (optional)"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-light-field-label">一句话描述</div>
|
<div className="_memory-light-field-label">One-line description</div>
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={tplDescription}
|
value={tplDescription}
|
||||||
onChange={setTplDescription}
|
onChange={setTplDescription}
|
||||||
placeholder="模板的一句话功能介绍(选填)"
|
placeholder="One-line description of what the template does (optional)"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-light-field-label">角色定位 prompt</div>
|
<div className="_memory-light-field-label">Role prompt</div>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
rows={3}
|
rows={3}
|
||||||
value={tplRolePrompt}
|
value={tplRolePrompt}
|
||||||
onChange={setTplRolePrompt}
|
onChange={setTplRolePrompt}
|
||||||
placeholder="role prompt · 这个 agent 扮演什么角色 / 职责定位(选填)"
|
placeholder="role prompt · what role/responsibility this agent plays (optional)"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-light-field-label">规则固定 prompt</div>
|
<div className="_memory-light-field-label">Rules prompt</div>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
rows={4}
|
rows={4}
|
||||||
value={tplRulesPrompt}
|
value={tplRulesPrompt}
|
||||||
onChange={setTplRulesPrompt}
|
onChange={setTplRulesPrompt}
|
||||||
placeholder={'rules prompt · 硬约束,建议编号列表(选填)\n1. …\n2. …'}
|
placeholder={'rules prompt · hard constraints, numbered list recommended (optional)\n1. …\n2. …'}
|
||||||
className="_memory-mono-textarea"
|
className="_memory-mono-textarea"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="_memory-template-save-actions">
|
<div className="_memory-template-save-actions">
|
||||||
<Button onClick={resetSaveTemplateForm}>取消</Button>
|
<Button onClick={resetSaveTemplateForm}>Cancel</Button>
|
||||||
<Button type="primary"
|
<Button type="primary"
|
||||||
disabled={!tplName.trim()}
|
disabled={!tplName.trim()}
|
||||||
onClick={handleSaveTemplate}
|
onClick={handleSaveTemplate}
|
||||||
>
|
>
|
||||||
保存模板
|
Save template
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LightField label="名字 *">
|
<LightField label="Name *">
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
size="full"
|
size="full"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
placeholder="如 Code Reviewer"
|
placeholder="e.g. Code Reviewer"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-field-hint">
|
<div className="_memory-field-hint">
|
||||||
agent_id 由后端生成并保证全局唯一(不限于本 team)。
|
agent_id is generated by the backend and guaranteed globally unique (not limited to this team).
|
||||||
</div>
|
</div>
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField label="一句话描述" hint="选填 · 留空也可以,详情页随时改。">
|
<LightField label="One-line description" hint="Optional · can be left blank, editable anytime from the detail page.">
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={setDescription}
|
onChange={setDescription}
|
||||||
placeholder="一句话功能介绍:这个 agent 是干什么的?(选填)"
|
placeholder="One-line description: what does this agent do? (optional)"
|
||||||
/>
|
/>
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField
|
<LightField
|
||||||
label="角色定位 prompt"
|
label="Role prompt"
|
||||||
hint="role prompt · 选填 · 描述这个 agent 扮演什么角色 / 职责定位,创建后可在详情页补。"
|
hint="role prompt · optional · describe what role/responsibility this agent plays, can be filled in later on the detail page."
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={rolePrompt}
|
value={rolePrompt}
|
||||||
onChange={setRolePrompt}
|
onChange={setRolePrompt}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="如:你是严格的 PR Reviewer,是代码合入主干前的最后一道质量关卡。"
|
placeholder="e.g. You are a strict PR reviewer, the last quality gate before code merges into main."
|
||||||
/>
|
/>
|
||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
<LightField
|
<LightField
|
||||||
label="规则固定 prompt"
|
label="Rules prompt"
|
||||||
hint="rules prompt · 选填 · 注入到每次对话开头的硬约束,建议用编号列表,创建后可在详情页补。"
|
hint="rules prompt · optional · hard constraints injected at the start of every conversation, numbered list recommended, can be filled in later on the detail page."
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
@ -298,11 +298,11 @@ export default function CreateAgentDialog({
|
|||||||
</LightField>
|
</LightField>
|
||||||
|
|
||||||
{assets.loading ? (
|
{assets.loading ? (
|
||||||
<div className="_memory-asset-loading">加载团队资产中…</div>
|
<div className="_memory-asset-loading">Loading team assets…</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="_memory-asset-toolbar">
|
<div className="_memory-asset-toolbar">
|
||||||
<span className="_memory-asset-toolbar-label">原子能力:</span>
|
<span className="_memory-asset-toolbar-label">Atomic capabilities:</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -313,7 +313,7 @@ export default function CreateAgentDialog({
|
|||||||
}}
|
}}
|
||||||
className="_memory-asset-toolbar-btn"
|
className="_memory-asset-toolbar-btn"
|
||||||
>
|
>
|
||||||
一键全选
|
Select all
|
||||||
</button>
|
</button>
|
||||||
{totalSelected > 0 && (
|
{totalSelected > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -326,13 +326,13 @@ export default function CreateAgentDialog({
|
|||||||
}}
|
}}
|
||||||
className="_memory-asset-toolbar-btn"
|
className="_memory-asset-toolbar-btn"
|
||||||
>
|
>
|
||||||
清空
|
Clear
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CollapseGroup
|
<CollapseGroup
|
||||||
icon={<BooksIcon size={16} />}
|
icon={<BooksIcon size={16} />}
|
||||||
title="Wiki 知识库"
|
title="Wiki knowledge base"
|
||||||
selectedCount={llmWikis.length}
|
selectedCount={llmWikis.length}
|
||||||
totalCount={assets.wikis.length}
|
totalCount={assets.wikis.length}
|
||||||
open={wikiOpen}
|
open={wikiOpen}
|
||||||
@ -360,7 +360,7 @@ export default function CreateAgentDialog({
|
|||||||
</CollapseGroup>
|
</CollapseGroup>
|
||||||
<CollapseGroup
|
<CollapseGroup
|
||||||
icon={<ToolsIcon size={16} />}
|
icon={<ToolsIcon size={16} />}
|
||||||
title="Skill 技能"
|
title="Skill"
|
||||||
selectedCount={skills.length}
|
selectedCount={skills.length}
|
||||||
totalCount={assets.skills.length}
|
totalCount={assets.skills.length}
|
||||||
open={skillsOpen}
|
open={skillsOpen}
|
||||||
@ -406,9 +406,9 @@ export default function CreateAgentDialog({
|
|||||||
chatMemories,
|
chatMemories,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
创建
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} disabled={busy}>取消</Button>
|
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,32 +18,32 @@ export default function CreateTeamDialog({
|
|||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const canSubmit = name.trim().length > 0 && !busy;
|
const canSubmit = name.trim().length > 0 && !busy;
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="创建 Team" size="s" onClose={onClose} disableEscape={busy}>
|
<Modal visible caption="Create team" size="s" onClose={onClose} disableEscape={busy}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="名称" required extra="Team 是资产、agent 和 task 的主要边界。">
|
<Form.Item label="Name" required extra="A team is the primary boundary for assets, agents, and tasks.">
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
size="full"
|
size="full"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
placeholder="例如 tdai-memory · 后端组"
|
placeholder="e.g. godcall-memory · backend team"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="描述">
|
<Form.Item label="Description">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={setDescription}
|
onChange={setDescription}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="一句话说明 team 范围与目标"
|
placeholder="One sentence describing the team's scope and goal"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" disabled={!canSubmit} loading={busy} onClick={() => onCreate({ name: name.trim(), description: description.trim() })}>创建</Button>
|
<Button type="primary" disabled={!canSubmit} loading={busy} onClick={() => onCreate({ name: name.trim(), description: description.trim() })}>Create</Button>
|
||||||
<Button onClick={onClose} disabled={busy}>取消</Button>
|
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -30,9 +30,9 @@ export function MemberSection({
|
|||||||
|
|
||||||
async function handleRemove(userId: string) {
|
async function handleRemove(userId: string) {
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `移除成员 ${userId}?`,
|
message: `Remove member ${userId}?`,
|
||||||
description: '此操作仅将该用户移出当前团队,不会删除用户账号。',
|
description: 'This only removes the user from the current team; it does not delete the user account.',
|
||||||
okText: '移除',
|
okText: 'Remove',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
setRemoving(userId);
|
setRemoving(userId);
|
||||||
@ -51,17 +51,17 @@ export function MemberSection({
|
|||||||
<div className="_memory-section-header">
|
<div className="_memory-section-header">
|
||||||
<div className="_memory-section-header-info">
|
<div className="_memory-section-header-info">
|
||||||
<div className="_memory-section-header-title-row">
|
<div className="_memory-section-header-title-row">
|
||||||
<div className="_memory-section-title">成员({team.members.length})</div>
|
<div className="_memory-section-title">Members ({team.members.length})</div>
|
||||||
<Tag size="sm">{team.team_id}</Tag>
|
<Tag size="sm">{team.team_id}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-section-subtitle">
|
<div className="_memory-section-subtitle">
|
||||||
「{team.name}」的人类成员;admin 可管理 team 资产,member 可使用资产并创建 task ·
|
Human members of "{team.name}"; admins can manage team assets, members can use assets
|
||||||
点击卡片查看详情
|
and create tasks · click a card for details
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canAddMember && (
|
{canAddMember && (
|
||||||
<Button onClick={onAdd} title="按 user_id 邀请成员加入">
|
<Button onClick={onAdd} title="Invite a member by user_id">
|
||||||
<AddIcon size={14} /> 添加成员
|
<AddIcon size={14} /> Add member
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -118,7 +118,7 @@ function MemberCard({
|
|||||||
<div className="_memory-member-info">
|
<div className="_memory-member-info">
|
||||||
<div className="_memory-member-id">
|
<div className="_memory-member-id">
|
||||||
{displayName}
|
{displayName}
|
||||||
{isMe && <span className="_memory-member-me-tag"> (你)</span>}
|
{isMe && <span className="_memory-member-me-tag"> (you)</span>}
|
||||||
</div>
|
</div>
|
||||||
{hasUsername && (
|
{hasUsername && (
|
||||||
<div className="_memory-member-role" style={{ fontSize: '10px', color: 'var(--tea-color-text-tertiary)' }}>
|
<div className="_memory-member-role" style={{ fontSize: '10px', color: 'var(--tea-color-text-tertiary)' }}>
|
||||||
@ -127,7 +127,7 @@ function MemberCard({
|
|||||||
)}
|
)}
|
||||||
<div className="_memory-member-role">
|
<div className="_memory-member-role">
|
||||||
{role}
|
{role}
|
||||||
{isOwner ? ' · 创建者' : ''}
|
{isOwner ? ' · Creator' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-member-actions">
|
<div className="_memory-member-actions">
|
||||||
@ -137,8 +137,8 @@ function MemberCard({
|
|||||||
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
||||||
disabled={removing}
|
disabled={removing}
|
||||||
className="_memory-member-remove-btn"
|
className="_memory-member-remove-btn"
|
||||||
title="移除该成员"
|
title="Remove this member"
|
||||||
aria-label="移除该成员"
|
aria-label="Remove this member"
|
||||||
>
|
>
|
||||||
{removing ? '…' : <CloseIcon size={12} />}
|
{removing ? '…' : <CloseIcon size={12} />}
|
||||||
</button>
|
</button>
|
||||||
@ -188,11 +188,11 @@ export function AddMemberDialog({
|
|||||||
async function submitExisting() {
|
async function submitExisting() {
|
||||||
const id = userId.trim();
|
const id = userId.trim();
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setError('请输入对方的 user_id。');
|
setError('Enter the user_id of the person to add.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (id === currentUser) {
|
if (id === currentUser) {
|
||||||
setError('不能添加自己;如需调整角色,请由其他 team admin 操作。');
|
setError('You cannot add yourself; ask another team admin to adjust roles.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -211,12 +211,12 @@ export function AddMemberDialog({
|
|||||||
async function submitNew() {
|
async function submitNew() {
|
||||||
const username = newUsername.trim();
|
const username = newUsername.trim();
|
||||||
if (!username) {
|
if (!username) {
|
||||||
setError('请输入用户名。');
|
setError('Enter a username.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 用户名只允许英文字母、数字、下划线(与后端 user_id 段校验规则一致)
|
// 用户名只允许英文字母、数字、下划线(与后端 user_id 段校验规则一致)
|
||||||
if (!/^[A-Za-z0-9_]+$/.test(username)) {
|
if (!/^[A-Za-z0-9_]+$/.test(username)) {
|
||||||
setError('用户名仅支持英文字母、数字、下划线,不能包含其他符号或空格。');
|
setError('Usernames may only contain letters, numbers, and underscores — no other symbols or spaces.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -260,15 +260,15 @@ export function AddMemberDialog({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
caption={<>添加成员到「{team.name}」<Tag size="sm">{team.team_id}</Tag></>}
|
caption={<>Add member to "{team.name}"<Tag size="sm">{team.team_id}</Tag></>}
|
||||||
size="m"
|
size="m"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
disableEscape={submitting}
|
disableEscape={submitting}
|
||||||
>
|
>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{!canGrantAdmin && <Alert type="info">仅 team admin 可授予 admin 角色</Alert>}
|
{!canGrantAdmin && <Alert type="info">Only team admins can grant the admin role</Alert>}
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="方式">
|
<Form.Item label="Method">
|
||||||
{canCreateUser ? (
|
{canCreateUser ? (
|
||||||
<Segment
|
<Segment
|
||||||
value={mode}
|
value={mode}
|
||||||
@ -278,13 +278,13 @@ export function AddMemberDialog({
|
|||||||
if (v === 'new') setRole('member');
|
if (v === 'new') setRole('member');
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'existing', text: '添加已有用户' },
|
{ value: 'existing', text: 'Add existing user' },
|
||||||
{ value: 'new', text: '新建用户并加入团队' },
|
{ value: 'new', text: 'Create user and add to team' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="_memory-field-hint">
|
<div className="_memory-field-hint">
|
||||||
添加已有用户(按 user_id 邀请加入团队)。新建用户账号须全局 admin 权限。
|
Add an existing user (invite by user_id to join the team). Creating a new user account requires global admin permission.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -301,14 +301,14 @@ export function AddMemberDialog({
|
|||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
onPressEnter={() => void handleSubmit()}
|
onPressEnter={() => void handleSubmit()}
|
||||||
placeholder="例如 usr-xxxxxxxxxxxx"
|
placeholder="e.g. usr-xxxxxxxxxxxx"
|
||||||
/>
|
/>
|
||||||
<div className="_memory-field-hint">可让对方在「我的资料」里复制发给你</div>
|
<div className="_memory-field-hint">They can copy this from "My profile" and send it to you</div>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="用户名" required>
|
<Form.Item label="Username" required>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
@ -319,39 +319,39 @@ export function AddMemberDialog({
|
|||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
onPressEnter={() => void handleSubmit()}
|
onPressEnter={() => void handleSubmit()}
|
||||||
placeholder="例如 alice"
|
placeholder="e.g. alice"
|
||||||
/>
|
/>
|
||||||
{newUsername.trim() && !/^[A-Za-z0-9_]+$/.test(newUsername.trim()) ? (
|
{newUsername.trim() && !/^[A-Za-z0-9_]+$/.test(newUsername.trim()) ? (
|
||||||
<div className="_memory-field-hint" style={{ color: 'var(--tea-color-text-error-default)' }}>
|
<div className="_memory-field-hint" style={{ color: 'var(--tea-color-text-error-default)' }}>
|
||||||
仅支持英文字母、数字、下划线,不能包含空格或其他符号
|
Only letters, numbers, and underscores are allowed — no spaces or other symbols
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="_memory-field-hint">英文字母、数字、下划线,创建后不可修改</div>
|
<div className="_memory-field-hint">Letters, numbers, underscores; cannot be changed after creation</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label="角色">
|
<Form.Item label="Role">
|
||||||
<Select
|
<Select
|
||||||
size="full"
|
size="full"
|
||||||
value="member"
|
value="member"
|
||||||
disabled
|
disabled
|
||||||
options={[
|
options={[
|
||||||
{ value: 'member', text: 'member(默认)' },
|
{ value: 'member', text: 'member (default)' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="_memory-field-hint">新成员默认角色为 member。</div>
|
<div className="_memory-field-hint">New members default to the member role.</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void handleSubmit()} disabled={!canSubmit || submitting} loading={submitting}>
|
<Button type="primary" onClick={() => void handleSubmit()} disabled={!canSubmit || submitting} loading={submitting}>
|
||||||
{mode === 'existing' ? '添加' : '新建并添加'}
|
{mode === 'existing' ? 'Add' : 'Create and add'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} disabled={submitting}>取消</Button>
|
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -370,16 +370,16 @@ export function CreatedUserKeyModal({
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="用户创建成功" size="m" onClose={onClose}>
|
<Modal visible caption="User created successfully" size="m" onClose={onClose}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Alert type="success">用户 {info.username}({info.userId})已创建并加入团队。</Alert>
|
<Alert type="success">User {info.username} ({info.userId}) has been created and added to the team.</Alert>
|
||||||
<div className="space-y-4 text-[13px]">
|
<div className="space-y-4 text-[13px]">
|
||||||
{info.keyValue ? (
|
{info.keyValue ? (
|
||||||
<>
|
<>
|
||||||
<Alert type="warning">
|
<Alert type="warning">
|
||||||
<strong>以下 User_Key 仅显示这一次</strong>,请立即复制并安全地发送给该用户。
|
<strong>The User_Key below is shown only this once</strong> — copy it now and send it
|
||||||
关闭此弹窗后无法再次查看该 Key。
|
securely to the user. Once this dialog is closed, the key cannot be viewed again.
|
||||||
</Alert>
|
</Alert>
|
||||||
<Form.Item label="User_Key">
|
<Form.Item label="User_Key">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -388,7 +388,7 @@ export function CreatedUserKeyModal({
|
|||||||
</code>
|
</code>
|
||||||
<Copy text={info.keyValue}>
|
<Copy text={info.keyValue}>
|
||||||
<Button onClick={() => setCopied(true)}>
|
<Button onClick={() => setCopied(true)}>
|
||||||
{copied ? '已复制' : '复制'}
|
{copied ? 'Copied' : 'Copy'}
|
||||||
</Button>
|
</Button>
|
||||||
</Copy>
|
</Copy>
|
||||||
</div>
|
</div>
|
||||||
@ -396,8 +396,8 @@ export function CreatedUserKeyModal({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Alert type="warning">
|
<Alert type="warning">
|
||||||
未能自动生成初始 User_Key。请让该用户使用以下 user_id 登录后自行在「User_Key
|
Failed to auto-generate an initial User_Key. Have the user sign in with the following
|
||||||
管理」中创建:
|
user_id and create one themselves in "User_Key management":
|
||||||
<code
|
<code
|
||||||
className="mt-1 block rounded px-2 py-1 text-[12px] font-mono select-all"
|
className="mt-1 block rounded px-2 py-1 text-[12px] font-mono select-all"
|
||||||
style={{ background: 'var(--tea-color-bg-primary-default)' }}
|
style={{ background: 'var(--tea-color-bg-primary-default)' }}
|
||||||
@ -410,7 +410,7 @@ export function CreatedUserKeyModal({
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={onClose}>我知道了</Button>
|
<Button type="primary" onClick={onClose}>Got it</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -174,14 +174,14 @@ export default function TeamManagementPanel({
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
tea.notify.error(
|
tea.notify.error(
|
||||||
`你不是 agent「${agent.name}」(${agent.agent_id}) 的 owner,也不是 team「${activeTeam.name}」的管理员,无法删除。owner: ${agent.owner_user_id || '(未设置)'}`,
|
`You are not the owner of agent "${agent.name}" (${agent.agent_id}), nor an admin of team "${activeTeam.name}", so you cannot delete it. Owner: ${agent.owner_user_id || '(not set)'}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认删除 Agent「${agent.name}」?`,
|
message: `Delete agent "${agent.name}"?`,
|
||||||
description: `${agent.agent_id} 删除后不可恢复。`,
|
description: `${agent.agent_id} cannot be recovered after deletion.`,
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
@ -193,7 +193,7 @@ export default function TeamManagementPanel({
|
|||||||
const raw = err instanceof Error ? err.message : String(err);
|
const raw = err instanceof Error ? err.message : String(err);
|
||||||
if (raw.includes('SKILL_DELETE_FAILED')) {
|
if (raw.includes('SKILL_DELETE_FAILED')) {
|
||||||
tea.notify.error(
|
tea.notify.error(
|
||||||
`Agent「${agent.name}」未删除:级联删除 Skill 中途失败。请到 Skill 面板检查并重试。原始错误:${raw}`,
|
`Agent "${agent.name}" was not deleted: cascading skill deletion failed partway through. Check the Skill panel and retry. Original error: ${raw}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tea.notify.error(errMsg(err));
|
tea.notify.error(errMsg(err));
|
||||||
@ -237,7 +237,7 @@ export default function TeamManagementPanel({
|
|||||||
className="_memory-team-header-name"
|
className="_memory-team-header-name"
|
||||||
style={{ color: 'var(--muted-foreground)' }}
|
style={{ color: 'var(--muted-foreground)' }}
|
||||||
>
|
>
|
||||||
加载中…
|
Loading…
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -251,7 +251,7 @@ export default function TeamManagementPanel({
|
|||||||
<div className="_memory-team-header-meta-row">
|
<div className="_memory-team-header-meta-row">
|
||||||
<span className="_memory-team-header-name">{activeTeam.name}</span>
|
<span className="_memory-team-header-name">{activeTeam.name}</span>
|
||||||
<Tag size="sm">{activeTeam.team_id}</Tag>
|
<Tag size="sm">{activeTeam.team_id}</Tag>
|
||||||
<span className="_memory-team-header-count">{activeTeam.members.length} 人</span>
|
<span className="_memory-team-header-count">{activeTeam.members.length} members</span>
|
||||||
</div>
|
</div>
|
||||||
{activeTeam.description && (
|
{activeTeam.description && (
|
||||||
<div className="_memory-team-header-desc">{activeTeam.description}</div>
|
<div className="_memory-team-header-desc">{activeTeam.description}</div>
|
||||||
@ -260,24 +260,24 @@ export default function TeamManagementPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="_memory-team-header-empty-hint">
|
<div className="_memory-team-header-empty-hint">
|
||||||
请在右上角选择一个 team,或新建一个 team 开始。
|
Select a team in the top-right corner, or create a new team to get started.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="_memory-team-header-ops">
|
<div className="_memory-team-header-ops">
|
||||||
{!teamsLoading && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
|
{!teamsLoading && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
|
||||||
<Button onClick={() => setShowCreateTeam(true)} title="创建一个新 team">
|
<Button onClick={() => setShowCreateTeam(true)} title="Create a new team">
|
||||||
<AddIcon size={14} /> 新建 Team
|
<AddIcon size={14} /> Create team
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{activeTeam && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
|
{activeTeam && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
tea.notify.warning('团队删除功能尚未在后端稳定支持,请联系管理员处理。')
|
tea.notify.warning("Team deletion isn't yet stably supported on the backend. Please contact an administrator.")
|
||||||
}
|
}
|
||||||
title="团队删除功能尚未在后端稳定支持"
|
title="Team deletion isn't yet stably supported on the backend"
|
||||||
>
|
>
|
||||||
删除当前 Team
|
Delete current team
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -289,7 +289,7 @@ export default function TeamManagementPanel({
|
|||||||
className="_memory-panel-card"
|
className="_memory-panel-card"
|
||||||
style={{ padding: '2rem', textAlign: 'center', color: 'var(--muted-foreground)' }}
|
style={{ padding: '2rem', textAlign: 'center', color: 'var(--muted-foreground)' }}
|
||||||
>
|
>
|
||||||
加载中…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
) : !activeTeam ? (
|
) : !activeTeam ? (
|
||||||
<EmptyTeamState onCreateTeam={() => setShowCreateTeam(true)} />
|
<EmptyTeamState onCreateTeam={() => setShowCreateTeam(true)} />
|
||||||
@ -375,13 +375,13 @@ function EmptyTeamState({ onCreateTeam }: { onCreateTeam: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<div className="_memory-empty-team">
|
<div className="_memory-empty-team">
|
||||||
<UsergroupIcon size={32} className="_memory-empty-team-icon" />
|
<UsergroupIcon size={32} className="_memory-empty-team-icon" />
|
||||||
<div className="_memory-empty-team-title">还没有归属任何 Team</div>
|
<div className="_memory-empty-team-title">You don't belong to any team yet</div>
|
||||||
<div className="_memory-empty-team-desc">
|
<div className="_memory-empty-team-desc">
|
||||||
Team 是资产、agent 和 task 的主要边界。先创建一个 team 来开始,或联系已有 team
|
A team is the primary boundary for assets, agents, and tasks. Create a team to get
|
||||||
的管理员把你加入。
|
started, or ask the admin of an existing team to add you.
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" onClick={onCreateTeam} className="_memory-empty-team-cta">
|
<Button type="primary" onClick={onCreateTeam} className="_memory-empty-team-cta">
|
||||||
<AddIcon size={14} /> 创建第一个 Team
|
<AddIcon size={14} /> Create your first team
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export function CollapseGroup({
|
|||||||
<span className="_memory-collapse-group-icon">{icon}</span>
|
<span className="_memory-collapse-group-icon">{icon}</span>
|
||||||
<span className="_memory-collapse-group-title">{title}</span>
|
<span className="_memory-collapse-group-title">{title}</span>
|
||||||
<span className="_memory-collapse-group-count">
|
<span className="_memory-collapse-group-count">
|
||||||
{hideTotal ? `已绑定 ${selectedCount}` : `已选 ${selectedCount} / 共 ${totalCount}`}
|
{hideTotal ? `Bound ${selectedCount}` : `${selectedCount} selected / ${totalCount} total`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{open && <div className="_memory-collapse-group-body">{children}</div>}
|
{open && <div className="_memory-collapse-group-body">{children}</div>}
|
||||||
@ -112,7 +112,7 @@ export function AssetCheckList({
|
|||||||
<span className="_memory-asset-check-item-row">
|
<span className="_memory-asset-check-item-row">
|
||||||
<span className="_memory-asset-check-item-title">{a.title}</span>
|
<span className="_memory-asset-check-item-title">{a.title}</span>
|
||||||
<span className="_memory-asset-check-item-slug">
|
<span className="_memory-asset-check-item-slug">
|
||||||
{a.slug}{disabledKeys.has(a.key) ? ' · 自身记忆,固定保留' : notReady ? ` · ${a.status}` : ''}
|
{a.slug}{disabledKeys.has(a.key) ? ' · own memory, always retained' : notReady ? ` · ${a.status}` : ''}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@ -213,8 +213,8 @@ export default function KnowledgeGraph({ data, loading, onNodeClick, highlightNo
|
|||||||
setSearchResults(filteredData.nodes.filter((n) => n.label.toLowerCase().includes(lower) || n.id.toLowerCase().includes(lower)).slice(0, 8));
|
setSearchResults(filteredData.nodes.filter((n) => n.label.toLowerCase().includes(lower) || n.id.toLowerCase().includes(lower)).slice(0, 8));
|
||||||
}, [filteredData]);
|
}, [filteredData]);
|
||||||
|
|
||||||
if (loading) return <div className={`flex items-center justify-center ${className}`} style={{ background: palette.bg }}><span className="text-xs text-muted-foreground">加载图谱...</span></div>;
|
if (loading) return <div className={`flex items-center justify-center ${className}`} style={{ background: palette.bg }}><span className="text-xs text-muted-foreground">Loading graph...</span></div>;
|
||||||
if (!filteredData || filteredData.nodes.length === 0) return <div className={`flex items-center justify-center ${className}`} style={{ background: palette.bg }}><span className="text-[12px] text-muted-foreground/70">暂无图谱数据</span></div>;
|
if (!filteredData || filteredData.nodes.length === 0) return <div className={`flex items-center justify-center ${className}`} style={{ background: palette.bg }}><span className="text-[12px] text-muted-foreground/70">No graph data</span></div>;
|
||||||
|
|
||||||
const typeSet = new Set(filteredData.nodes.map((n) => n.type));
|
const typeSet = new Set(filteredData.nodes.map((n) => n.type));
|
||||||
const types = [...typeSet].sort();
|
const types = [...typeSet].sort();
|
||||||
@ -235,7 +235,7 @@ export default function KnowledgeGraph({ data, loading, onNodeClick, highlightNo
|
|||||||
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground/70 text-xs inline-flex items-center"><SearchIcon size={12} /></span>
|
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground/70 text-xs inline-flex items-center"><SearchIcon size={12} /></span>
|
||||||
<input
|
<input
|
||||||
className="h-7 w-full pl-7 pr-6 text-xs border rounded-md bg-card/80 border-border text-foreground/70 placeholder:text-muted-foreground/70 focus:outline-none focus:ring-1 focus:ring-primary"
|
className="h-7 w-full pl-7 pr-6 text-xs border rounded-md bg-card/80 border-border text-foreground/70 placeholder:text-muted-foreground/70 focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
placeholder="搜索节点..."
|
placeholder="Search nodes..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -244,11 +244,11 @@ export default function KnowledgeGraph({ data, loading, onNodeClick, highlightNo
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-0.5">
|
<div className="flex gap-0.5">
|
||||||
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${colorMode === "type" ? "bg-primary/5 text-primary ring-1 ring-primary/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`} onClick={() => setColorMode("type")}>类型</button>
|
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${colorMode === "type" ? "bg-primary/5 text-primary ring-1 ring-primary/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`} onClick={() => setColorMode("type")}>Type</button>
|
||||||
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${colorMode === "community" ? "bg-primary/5 text-primary ring-1 ring-primary/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`} onClick={() => setColorMode("community")}>社区</button>
|
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${colorMode === "community" ? "bg-primary/5 text-primary ring-1 ring-primary/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`} onClick={() => setColorMode("community")}>Community</button>
|
||||||
</div>
|
</div>
|
||||||
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${hideStructural ? "bg-success/10 text-success ring-1 ring-success/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`}
|
<button className={`rounded-md px-2 py-1 text-xs font-medium transition ${hideStructural ? "bg-success/10 text-success ring-1 ring-success/30" : "text-muted-foreground hover:text-foreground/70 hover:bg-muted"}`}
|
||||||
onClick={() => setHideStructural(!hideStructural)} title="隐藏结构性节点">隐藏结构</button>
|
onClick={() => setHideStructural(!hideStructural)} title="Hide structural nodes">Hide structural</button>
|
||||||
<span className="text-xs ml-auto font-mono text-muted-foreground">{filteredData.nodes.length} nodes · {filteredData.edges.length} edges</span>
|
<span className="text-xs ml-auto font-mono text-muted-foreground">{filteredData.nodes.length} nodes · {filteredData.edges.length} edges</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -168,12 +168,12 @@ const WIKI_STATUS_BADGE: Record<
|
|||||||
WikiDetail['status'],
|
WikiDetail['status'],
|
||||||
{ label: string; theme: 'warning' | 'success' | 'error' | 'default' }
|
{ label: string; theme: 'warning' | 'success' | 'error' | 'default' }
|
||||||
> = {
|
> = {
|
||||||
draft: { label: '待加工', theme: 'warning' },
|
draft: { label: 'Awaiting processing', theme: 'warning' },
|
||||||
pending: { label: '排队中', theme: 'warning' },
|
pending: { label: 'Queued', theme: 'warning' },
|
||||||
processing: { label: '加工中', theme: 'warning' },
|
processing: { label: 'Processing', theme: 'warning' },
|
||||||
ready: { label: '就绪', theme: 'success' },
|
ready: { label: 'Ready', theme: 'success' },
|
||||||
failed: { label: '失败', theme: 'error' },
|
failed: { label: 'Failed', theme: 'error' },
|
||||||
missing: { label: '已丢失', theme: 'error' },
|
missing: { label: 'Lost', theme: 'error' },
|
||||||
};
|
};
|
||||||
function WikiStatusBadge({ status }: { status: WikiDetail['status'] }) {
|
function WikiStatusBadge({ status }: { status: WikiDetail['status'] }) {
|
||||||
const b = WIKI_STATUS_BADGE[status] ?? { label: status, theme: 'default' as const };
|
const b = WIKI_STATUS_BADGE[status] ?? { label: status, theme: 'default' as const };
|
||||||
@ -226,10 +226,10 @@ const TYPE_COLOR_FALLBACK = 'var(--tea-color-text-tertiary)';
|
|||||||
|
|
||||||
type WikiScopeTab = 'all' | 'team' | 'fixed' | 'scope';
|
type WikiScopeTab = 'all' | 'team' | 'fixed' | 'scope';
|
||||||
const SCOPE_LABELS: Record<WikiScopeTab, string> = {
|
const SCOPE_LABELS: Record<WikiScopeTab, string> = {
|
||||||
all: '全部',
|
all: 'All',
|
||||||
team: '团队 Wiki 池',
|
team: 'Team wiki pool',
|
||||||
fixed: 'Agent 资产',
|
fixed: 'Agent assets',
|
||||||
scope: '可配置范围',
|
scope: 'Configurable scope',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -243,7 +243,7 @@ function WikiOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
|
|||||||
return (
|
return (
|
||||||
<span title={`Owner: ${userId}`}>
|
<span title={`Owner: ${userId}`}>
|
||||||
@{name || userId}
|
@{name || userId}
|
||||||
{userId === currentUserId && <span className="ml-1 text-xs text-primary">(你)</span>}
|
{userId === currentUserId && <span className="ml-1 text-xs text-primary">(you)</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ export default function WikiSourcesPanel() {
|
|||||||
const items = await knowledgeApi.wiki.agentFixed(agentFilter);
|
const items = await knowledgeApi.wiki.agentFixed(agentFilter);
|
||||||
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
|
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '加载固定资产失败');
|
tea.notify.error(e?.message || 'Failed to load fixed assets');
|
||||||
setFixedBoundIds(new Set());
|
setFixedBoundIds(new Set());
|
||||||
}
|
}
|
||||||
}, [agentFilter]);
|
}, [agentFilter]);
|
||||||
@ -480,7 +480,7 @@ export default function WikiSourcesPanel() {
|
|||||||
} finally {
|
} finally {
|
||||||
setGraphLoading(false);
|
setGraphLoading(false);
|
||||||
}
|
}
|
||||||
if (hadError) tea.notify.error('加载 Wiki 详情失败,部分内容可能不完整');
|
if (hadError) tea.notify.error('Failed to load wiki details. Some content may be incomplete.');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runningWikiKey = useMemo(
|
const runningWikiKey = useMemo(
|
||||||
@ -529,19 +529,19 @@ export default function WikiSourcesPanel() {
|
|||||||
async function handleUnbindWiki(wikiId: string) {
|
async function handleUnbindWiki(wikiId: string) {
|
||||||
if (!agentFilter) return;
|
if (!agentFilter) return;
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: '确认解绑该 Wiki?',
|
message: 'Unbind this wiki?',
|
||||||
description: '将从当前 agent 移除该 Wiki 绑定。',
|
description: 'This removes the wiki binding from the current agent.',
|
||||||
okText: '解绑',
|
okText: 'Unbind',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.unbind(wikiId, agentFilter);
|
await knowledgeApi.wiki.unbind(wikiId, agentFilter);
|
||||||
tea.notify.success('已解绑');
|
tea.notify.success('Unbound');
|
||||||
if (selectedWikiId === wikiId) setSelectedWikiId('');
|
if (selectedWikiId === wikiId) setSelectedWikiId('');
|
||||||
await fetchFixedBindings();
|
await fetchFixedBindings();
|
||||||
await fetchSources();
|
await fetchSources();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '解绑失败');
|
tea.notify.error(e?.message || 'Unbind failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,7 +551,7 @@ export default function WikiSourcesPanel() {
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.create(activeTeamId, newName.trim());
|
await knowledgeApi.wiki.create(activeTeamId, newName.trim());
|
||||||
tea.notify.success(`Wiki「${newName.trim()}」已创建`);
|
tea.notify.success(`Wiki "${newName.trim()}" created`);
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
setNewName('');
|
setNewName('');
|
||||||
fetchSources();
|
fetchSources();
|
||||||
@ -566,7 +566,7 @@ export default function WikiSourcesPanel() {
|
|||||||
// 防御:同一时间只允许一个 Wiki 提取,避免并发 ingest 导致后端排队混乱。
|
// 防御:同一时间只允许一个 Wiki 提取,避免并发 ingest 导致后端排队混乱。
|
||||||
// 按钮已按 ingestBusy 禁用,这里再挡一层防止绕过。
|
// 按钮已按 ingestBusy 禁用,这里再挡一层防止绕过。
|
||||||
if (ingestBusy) {
|
if (ingestBusy) {
|
||||||
tea.notify.warning('已有 Wiki 正在提取,请等待当前任务完成后再试。');
|
tea.notify.warning('A wiki is already being extracted. Wait for the current task to finish and try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wiki = sources.find((s) => s.wiki_id === wikiId);
|
const wiki = sources.find((s) => s.wiki_id === wikiId);
|
||||||
@ -576,7 +576,7 @@ export default function WikiSourcesPanel() {
|
|||||||
wikiId,
|
wikiId,
|
||||||
wiki: name,
|
wiki: name,
|
||||||
currentFile: '',
|
currentFile: '',
|
||||||
detail: '正在触发抽取...',
|
detail: 'Triggering extraction...',
|
||||||
done: 0,
|
done: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
checkCount: 0,
|
checkCount: 0,
|
||||||
@ -592,14 +592,14 @@ export default function WikiSourcesPanel() {
|
|||||||
const checkedAt = new Date(ev.ts).toLocaleTimeString();
|
const checkedAt = new Date(ev.ts).toLocaleTimeString();
|
||||||
if (ev.type === 'file_start') {
|
if (ev.type === 'file_start') {
|
||||||
next.currentFile = ev.file || '';
|
next.currentFile = ev.file || '';
|
||||||
next.detail = ev.detail || '处理中...';
|
next.detail = ev.detail || 'Processing...';
|
||||||
next.done = ev.done ?? prev.done;
|
next.done = ev.done ?? prev.done;
|
||||||
next.total = ev.total ?? prev.total;
|
next.total = ev.total ?? prev.total;
|
||||||
next.lastCheckedAt = checkedAt;
|
next.lastCheckedAt = checkedAt;
|
||||||
} else if (ev.type === 'file_done') {
|
} else if (ev.type === 'file_done') {
|
||||||
next.done = ev.done ?? prev.done;
|
next.done = ev.done ?? prev.done;
|
||||||
next.total = ev.total ?? prev.total;
|
next.total = ev.total ?? prev.total;
|
||||||
next.detail = ev.detail || `已检查 ${next.done}/${next.total}`;
|
next.detail = ev.detail || `Checked ${next.done}/${next.total}`;
|
||||||
next.checkCount = prev.checkCount + 1;
|
next.checkCount = prev.checkCount + 1;
|
||||||
next.lastCheckedAt = checkedAt;
|
next.lastCheckedAt = checkedAt;
|
||||||
if (ev.file) next.log = [...prev.log, { file: ev.file, status: 'done' }];
|
if (ev.file) next.log = [...prev.log, { file: ev.file, status: 'done' }];
|
||||||
@ -612,7 +612,7 @@ export default function WikiSourcesPanel() {
|
|||||||
} else if (ev.type === 'batch_done') {
|
} else if (ev.type === 'batch_done') {
|
||||||
next.done = ev.done ?? 100;
|
next.done = ev.done ?? 100;
|
||||||
next.total = ev.total ?? 100;
|
next.total = ev.total ?? 100;
|
||||||
next.detail = ev.detail || '抽取完成';
|
next.detail = ev.detail || 'Extraction complete';
|
||||||
next.lastCheckedAt = checkedAt;
|
next.lastCheckedAt = checkedAt;
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
@ -624,30 +624,30 @@ export default function WikiSourcesPanel() {
|
|||||||
active: false,
|
active: false,
|
||||||
done: 100,
|
done: 100,
|
||||||
total: 100,
|
total: 100,
|
||||||
detail: `完成!当前 ${result.ingested} 页`,
|
detail: `Done! ${result.ingested} pages so far`,
|
||||||
currentFile: '',
|
currentFile: '',
|
||||||
}));
|
}));
|
||||||
tea.notify.success(`Wiki 抽取完成,共 ${result.ingested} 页`);
|
tea.notify.success(`Wiki extraction complete — ${result.ingested} pages total`);
|
||||||
fetchSources();
|
fetchSources();
|
||||||
fetchDetail(wikiId);
|
fetchDetail(wikiId);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setIngestState((prev) => ({ ...prev, active: false, detail: `错误: ${err}` }));
|
setIngestState((prev) => ({ ...prev, active: false, detail: `Error: ${err}` }));
|
||||||
tea.notify.error(err || 'Wiki 抽取失败');
|
tea.notify.error(err || 'Wiki extraction failed');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
activeTeamId ?? '',
|
activeTeamId ?? '',
|
||||||
);
|
);
|
||||||
setIngestState((prev) =>
|
setIngestState((prev) =>
|
||||||
prev.active
|
prev.active
|
||||||
? { ...prev, active: false, detail: prev.log.length > 0 ? '完成' : prev.detail }
|
? { ...prev, active: false, detail: prev.log.length > 0 ? 'Done' : prev.detail }
|
||||||
: prev,
|
: prev,
|
||||||
);
|
);
|
||||||
fetchSources();
|
fetchSources();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (wikiId: string, name: string) => {
|
const handleDelete = async (wikiId: string, name: string) => {
|
||||||
const ok = await tea.confirm({ message: `确定要删除 Wiki「${name}」吗?`, okText: '删除' });
|
const ok = await tea.confirm({ message: `Delete wiki "${name}"?`, okText: 'Delete' });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.delete(wikiId);
|
await knowledgeApi.wiki.delete(wikiId);
|
||||||
@ -689,7 +689,7 @@ export default function WikiSourcesPanel() {
|
|||||||
setReadContent(r?.content || '');
|
setReadContent(r?.content || '');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setReadContent('');
|
setReadContent('');
|
||||||
tea.notify.error(e?.message || '读取页面内容失败');
|
tea.notify.error(e?.message || 'Failed to read page content');
|
||||||
} finally {
|
} finally {
|
||||||
setReadLoading(false);
|
setReadLoading(false);
|
||||||
}
|
}
|
||||||
@ -699,42 +699,42 @@ export default function WikiSourcesPanel() {
|
|||||||
if (!selectedWikiId) return;
|
if (!selectedWikiId) return;
|
||||||
const ref = (page as any).id || page.path;
|
const ref = (page as any).id || page.path;
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认删除页面「${page.title || ref}」?`,
|
message: `Delete page "${page.title || ref}"?`,
|
||||||
description: '会删除该 wiki 页面并清理引用。',
|
description: 'This deletes the wiki page and cleans up references.',
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.pageDelete(selectedWikiId, [ref]);
|
await knowledgeApi.wiki.pageDelete(selectedWikiId, [ref]);
|
||||||
tea.notify.success('已删除页面');
|
tea.notify.success('Page deleted');
|
||||||
if (selectedPage && ((selectedPage as any).id || selectedPage.path) === ref) {
|
if (selectedPage && ((selectedPage as any).id || selectedPage.path) === ref) {
|
||||||
setSelectedPage(null);
|
setSelectedPage(null);
|
||||||
setReadContent('');
|
setReadContent('');
|
||||||
}
|
}
|
||||||
await fetchDetail(selectedWikiId);
|
await fetchDetail(selectedWikiId);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '删除页面失败');
|
tea.notify.error(e?.message || 'Failed to delete page');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRaw = async (filename: string) => {
|
const handleDeleteRaw = async (filename: string) => {
|
||||||
if (!selectedWikiId) return;
|
if (!selectedWikiId) return;
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认删除原始文档「${filename}」?`,
|
message: `Delete original document "${filename}"?`,
|
||||||
description: '会删除原始文档,并同步清理由它派生的页面。',
|
description: 'This deletes the original document and cleans up pages derived from it.',
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await knowledgeApi.wiki.rawDelete(selectedWikiId, [filename]);
|
await knowledgeApi.wiki.rawDelete(selectedWikiId, [filename]);
|
||||||
tea.notify.success('已删除原始文档');
|
tea.notify.success('Original document deleted');
|
||||||
if (selectedPage?.path === `raw/${filename}`) {
|
if (selectedPage?.path === `raw/${filename}`) {
|
||||||
setSelectedPage(null);
|
setSelectedPage(null);
|
||||||
setReadContent('');
|
setReadContent('');
|
||||||
}
|
}
|
||||||
await fetchDetail(selectedWikiId);
|
await fetchDetail(selectedWikiId);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
tea.notify.error(e?.message || '删除原始文档失败');
|
tea.notify.error(e?.message || 'Failed to delete original document');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -761,13 +761,13 @@ export default function WikiSourcesPanel() {
|
|||||||
if (existing.length === 0) return true;
|
if (existing.length === 0) return true;
|
||||||
|
|
||||||
return tea.confirm({
|
return tea.confirm({
|
||||||
message: `检测到 ${existing.length} 个同名文件`,
|
message: `Found ${existing.length} file(s) with the same name`,
|
||||||
description: `继续上传将覆盖原有内容:${formatOverwriteFilenames(existing)}`,
|
description: `Continuing will overwrite existing content: ${formatOverwriteFilenames(existing)}`,
|
||||||
okText: '覆盖并上传',
|
okText: 'Overwrite and upload',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
tea.notify.error(e instanceof Error ? e : '获取已有文档失败,已取消上传');
|
tea.notify.error(e instanceof Error ? e : 'Failed to fetch existing documents. Upload canceled.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -778,15 +778,15 @@ export default function WikiSourcesPanel() {
|
|||||||
*/
|
*/
|
||||||
const offerIngestAfterUpload = async (wikiId: string, uploadedCount: number) => {
|
const offerIngestAfterUpload = async (wikiId: string, uploadedCount: number) => {
|
||||||
const shouldIngest = await tea.confirm({
|
const shouldIngest = await tea.confirm({
|
||||||
message: `${uploadedCount} 个文档已上传`,
|
message: `${uploadedCount} document(s) uploaded`,
|
||||||
description: '文档尚未抽取为可检索页面。现在开始抽取后,才能在页面、图谱和搜索中使用这些内容。',
|
description: "Documents haven't been extracted into searchable pages yet. Start extraction to use this content in pages, the graph, and search.",
|
||||||
okText: '开始抽取',
|
okText: 'Start extraction',
|
||||||
cancelText: '稍后处理',
|
cancelText: 'Later',
|
||||||
});
|
});
|
||||||
if (shouldIngest) {
|
if (shouldIngest) {
|
||||||
void handleIngest(wikiId);
|
void handleIngest(wikiId);
|
||||||
} else {
|
} else {
|
||||||
tea.notify.info('文档已保存。需要时可点击 Wiki 详情页右上角的“开始抽取”。');
|
tea.notify.info('Documents saved. Click "Start extraction" in the top-right of the wiki detail page when you\'re ready.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -814,7 +814,7 @@ export default function WikiSourcesPanel() {
|
|||||||
uploadInFlightRef.current = false;
|
uploadInFlightRef.current = false;
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
if (failures.length === 0) {
|
if (failures.length === 0) {
|
||||||
tea.notify.success(`已上传 ${valid.length} 个文档`);
|
tea.notify.success(`${valid.length} document(s) uploaded`);
|
||||||
setMdDocs([{ filename: '', content: '' }]);
|
setMdDocs([{ filename: '', content: '' }]);
|
||||||
setShowAddDoc(false);
|
setShowAddDoc(false);
|
||||||
fetchDetail(selectedWikiId);
|
fetchDetail(selectedWikiId);
|
||||||
@ -827,8 +827,8 @@ export default function WikiSourcesPanel() {
|
|||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((f) => `${f.filename}: ${f.error}`)
|
.map((f) => `${f.filename}: ${f.error}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
const more = failures.length > 3 ? `\n…及其它 ${failures.length - 3} 个` : '';
|
const more = failures.length > 3 ? `\n...and ${failures.length - 3} more` : '';
|
||||||
tea.notify.error(`${okCount} 个成功,${failures.length} 个失败:\n${shown}${more}`);
|
tea.notify.error(`${okCount} succeeded, ${failures.length} failed:\n${shown}${more}`);
|
||||||
fetchDetail(selectedWikiId);
|
fetchDetail(selectedWikiId);
|
||||||
setRawRefreshKey((k) => k + 1);
|
setRawRefreshKey((k) => k + 1);
|
||||||
if (okCount > 0) await offerIngestAfterUpload(selectedWikiId, okCount);
|
if (okCount > 0) await offerIngestAfterUpload(selectedWikiId, okCount);
|
||||||
@ -868,7 +868,7 @@ export default function WikiSourcesPanel() {
|
|||||||
const failed = results.filter((r) => r.status === 'rejected').length;
|
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||||
const succeeded = results.length - failed;
|
const succeeded = results.length - failed;
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
tea.notify.success(`已上传 ${succeeded} 个文件`);
|
tea.notify.success(`${succeeded} file(s) uploaded`);
|
||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
setUploadProgress({});
|
setUploadProgress({});
|
||||||
setShowAddDoc(false);
|
setShowAddDoc(false);
|
||||||
@ -881,7 +881,7 @@ export default function WikiSourcesPanel() {
|
|||||||
if (r.status === 'rejected')
|
if (r.status === 'rejected')
|
||||||
setUploadProgress((prev) => ({ ...prev, [pendingFiles[i].name]: 'error' }));
|
setUploadProgress((prev) => ({ ...prev, [pendingFiles[i].name]: 'error' }));
|
||||||
});
|
});
|
||||||
tea.notify.error(`${succeeded} 个成功,${failed} 个失败`);
|
tea.notify.error(`${succeeded} succeeded, ${failed} failed`);
|
||||||
fetchDetail(selectedWikiId);
|
fetchDetail(selectedWikiId);
|
||||||
setRawRefreshKey((k) => k + 1);
|
setRawRefreshKey((k) => k + 1);
|
||||||
if (succeeded > 0) await offerIngestAfterUpload(selectedWikiId, succeeded);
|
if (succeeded > 0) await offerIngestAfterUpload(selectedWikiId, succeeded);
|
||||||
@ -925,13 +925,13 @@ export default function WikiSourcesPanel() {
|
|||||||
if (hasManualIngestState || !runningWiki) return ingestState;
|
if (hasManualIngestState || !runningWiki) return ingestState;
|
||||||
const stage = wikiStageLabel(runningWiki.status, runningWiki.internal_status);
|
const stage = wikiStageLabel(runningWiki.status, runningWiki.internal_status);
|
||||||
const pageHint =
|
const pageHint =
|
||||||
typeof runningWiki.page_count === 'number' ? `,当前 ${runningWiki.page_count} 页` : '';
|
typeof runningWiki.page_count === 'number' ? `, ${runningWiki.page_count} pages so far` : '';
|
||||||
return {
|
return {
|
||||||
active: true,
|
active: true,
|
||||||
wikiId: runningWiki.wiki_id ?? '',
|
wikiId: runningWiki.wiki_id ?? '',
|
||||||
wiki: runningWiki.name,
|
wiki: runningWiki.name,
|
||||||
currentFile: '',
|
currentFile: '',
|
||||||
detail: `状态恢复:${stage}${pageHint}`,
|
detail: `Status restored: ${stage}${pageHint}`,
|
||||||
done: wikiProgressPercent(runningWiki.status, runningWiki.internal_status),
|
done: wikiProgressPercent(runningWiki.status, runningWiki.internal_status),
|
||||||
total: 100,
|
total: 100,
|
||||||
checkCount: 0,
|
checkCount: 0,
|
||||||
@ -999,7 +999,7 @@ export default function WikiSourcesPanel() {
|
|||||||
<Card.Body className="_wiki-detail-header-body">
|
<Card.Body className="_wiki-detail-header-body">
|
||||||
<div className="_wiki-detail-breadcrumb">
|
<div className="_wiki-detail-breadcrumb">
|
||||||
<Button type="text" onClick={() => { fetchSources(); setSubView('list'); }}>
|
<Button type="text" onClick={() => { fetchSources(); setSubView('list'); }}>
|
||||||
<ArrowLeftIcon size={12} /> Wiki 知识库
|
<ArrowLeftIcon size={12} /> Wiki knowledge base
|
||||||
</Button>
|
</Button>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span>{wikiName}</span>
|
<span>{wikiName}</span>
|
||||||
@ -1009,7 +1009,7 @@ export default function WikiSourcesPanel() {
|
|||||||
<BooksIcon size={18} />
|
<BooksIcon size={18} />
|
||||||
<span className="_wiki-detail-title">{wikiName}</span>
|
<span className="_wiki-detail-title">{wikiName}</span>
|
||||||
{source && <WikiStatusBadge status={source.status} />}
|
{source && <WikiStatusBadge status={source.status} />}
|
||||||
<Text theme="label">{pages.length} 页</Text>
|
<Text theme="label">{pages.length} pages</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="_wiki-detail-header-actions">
|
<div className="_wiki-detail-header-actions">
|
||||||
<Button
|
<Button
|
||||||
@ -1019,7 +1019,7 @@ export default function WikiSourcesPanel() {
|
|||||||
setAddDocTab('file');
|
setAddDocTab('file');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AttachIcon size={14} /> 添加
|
<AttachIcon size={14} /> Add
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -1028,7 +1028,7 @@ export default function WikiSourcesPanel() {
|
|||||||
loading={ingestBusy && displayIngestState.wiki === wikiName}
|
loading={ingestBusy && displayIngestState.wiki === wikiName}
|
||||||
>
|
>
|
||||||
{ingestBusy && displayIngestState.wiki === wikiName ? (
|
{ingestBusy && displayIngestState.wiki === wikiName ? (
|
||||||
'处理中'
|
'Processing'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<StarIcon size={14} /> Ingest
|
<StarIcon size={14} /> Ingest
|
||||||
@ -1052,14 +1052,14 @@ export default function WikiSourcesPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<CheckCircleIcon size={14} />
|
<CheckCircleIcon size={14} />
|
||||||
)}{' '}
|
)}{' '}
|
||||||
Ingest:{displayIngestState.wiki}
|
Ingest: {displayIngestState.wiki}
|
||||||
</Text>
|
</Text>
|
||||||
{!displayIngestState.active && (
|
{!displayIngestState.active && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
onClick={() => setIngestState((state) => ({ ...state, log: [] }))}
|
onClick={() => setIngestState((state) => ({ ...state, log: [] }))}
|
||||||
>
|
>
|
||||||
清除
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1078,9 +1078,9 @@ export default function WikiSourcesPanel() {
|
|||||||
</div>
|
</div>
|
||||||
{displayIngestState.checkCount > 0 && (
|
{displayIngestState.checkCount > 0 && (
|
||||||
<Text theme="label">
|
<Text theme="label">
|
||||||
已实际查询 {displayIngestState.checkCount} 次
|
Checked {displayIngestState.checkCount} times
|
||||||
{displayIngestState.lastCheckedAt
|
{displayIngestState.lastCheckedAt
|
||||||
? `,最近 ${displayIngestState.lastCheckedAt}`
|
? `, last at ${displayIngestState.lastCheckedAt}`
|
||||||
: ''}
|
: ''}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -1124,7 +1124,7 @@ export default function WikiSourcesPanel() {
|
|||||||
label: (
|
label: (
|
||||||
<span className="_wiki-detail-tab-label">
|
<span className="_wiki-detail-tab-label">
|
||||||
<ChartBarIcon size={14} />
|
<ChartBarIcon size={14} />
|
||||||
概览
|
Overview
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1133,7 +1133,7 @@ export default function WikiSourcesPanel() {
|
|||||||
label: (
|
label: (
|
||||||
<span className="_wiki-detail-tab-label">
|
<span className="_wiki-detail-tab-label">
|
||||||
<ArchitectureIcon size={14} />
|
<ArchitectureIcon size={14} />
|
||||||
图谱
|
Graph
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1142,7 +1142,7 @@ export default function WikiSourcesPanel() {
|
|||||||
label: (
|
label: (
|
||||||
<span className="_wiki-detail-tab-label">
|
<span className="_wiki-detail-tab-label">
|
||||||
<FileIcon size={14} />
|
<FileIcon size={14} />
|
||||||
页面
|
Pages
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1151,7 +1151,7 @@ export default function WikiSourcesPanel() {
|
|||||||
label: (
|
label: (
|
||||||
<span className="_wiki-detail-tab-label">
|
<span className="_wiki-detail-tab-label">
|
||||||
<SearchIcon size={14} />
|
<SearchIcon size={14} />
|
||||||
搜索
|
Search
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1160,14 +1160,14 @@ export default function WikiSourcesPanel() {
|
|||||||
<TabPanel id="overview">
|
<TabPanel id="overview">
|
||||||
<div className="_wiki-detail-overview">
|
<div className="_wiki-detail-overview">
|
||||||
<div className="_wiki-detail-overview-stats">
|
<div className="_wiki-detail-overview-stats">
|
||||||
<MetricsBoard title="总页面数" value={pages.length} />
|
<MetricsBoard title="Total pages" value={pages.length} />
|
||||||
<MetricsBoard title="页面类型" value={types.length} />
|
<MetricsBoard title="Page types" value={types.length} />
|
||||||
<MetricsBoard title="页面间链接" value={edgeCount} />
|
<MetricsBoard title="Links between pages" value={edgeCount} />
|
||||||
</div>
|
</div>
|
||||||
<Card bordered>
|
<Card bordered>
|
||||||
<Card.Body title="类型分布">
|
<Card.Body title="Type distribution">
|
||||||
{types.length === 0 ? (
|
{types.length === 0 ? (
|
||||||
<StatusTip status="empty" emptyText="暂无页面数据" />
|
<StatusTip status="empty" emptyText="No page data" />
|
||||||
) : (
|
) : (
|
||||||
<div className="_wiki-detail-type-dist">
|
<div className="_wiki-detail-type-dist">
|
||||||
{types.map((type) => {
|
{types.map((type) => {
|
||||||
@ -1192,7 +1192,7 @@ export default function WikiSourcesPanel() {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Text theme="label" className="_wiki-detail-type-count">
|
<Text theme="label" className="_wiki-detail-type-count">
|
||||||
{count}({pct}%)
|
{count} ({pct}%)
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1202,9 +1202,9 @@ export default function WikiSourcesPanel() {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
<Card bordered>
|
<Card bordered>
|
||||||
<Card.Body title="页面一览">
|
<Card.Body title="Page overview">
|
||||||
{pages.length === 0 ? (
|
{pages.length === 0 ? (
|
||||||
<StatusTip status="empty" emptyText="暂无页面" />
|
<StatusTip status="empty" emptyText="No pages" />
|
||||||
) : (
|
) : (
|
||||||
<div className="_wiki-detail-overview-grid">
|
<div className="_wiki-detail-overview-grid">
|
||||||
{pages.slice(0, 9).map((page) => (
|
{pages.slice(0, 9).map((page) => (
|
||||||
@ -1278,7 +1278,7 @@ export default function WikiSourcesPanel() {
|
|||||||
.then((result: any) => setReadContent(result?.items?.[0]?.content || ''))
|
.then((result: any) => setReadContent(result?.items?.[0]?.content || ''))
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
setReadContent('');
|
setReadContent('');
|
||||||
tea.notify.error(error?.message || '读取原始文档失败');
|
tea.notify.error(error?.message || 'Failed to read original document');
|
||||||
})
|
})
|
||||||
.finally(() => setReadLoading(false));
|
.finally(() => setReadLoading(false));
|
||||||
}}
|
}}
|
||||||
@ -1290,12 +1290,12 @@ export default function WikiSourcesPanel() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={setSearchQuery}
|
onChange={setSearchQuery}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
placeholder="搜索文档内容…"
|
placeholder="Search document content..."
|
||||||
/>
|
/>
|
||||||
{searching && <StatusTip status="loading" />}
|
{searching && <StatusTip status="loading" />}
|
||||||
{!searching && searchResults.length > 0 && (
|
{!searching && searchResults.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text theme="label">{searchResults.length} 条结果</Text>
|
<Text theme="label">{searchResults.length} results</Text>
|
||||||
<div className="_wiki-detail-search-results">
|
<div className="_wiki-detail-search-results">
|
||||||
{searchResults.map((result, index) => (
|
{searchResults.map((result, index) => (
|
||||||
<button
|
<button
|
||||||
@ -1336,7 +1336,7 @@ export default function WikiSourcesPanel() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!searching && searchResults.length === 0 && searchQuery && (
|
{!searching && searchResults.length === 0 && searchQuery && (
|
||||||
<StatusTip status="empty" emptyText="未找到匹配结果" />
|
<StatusTip status="empty" emptyText="No matching results found" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
@ -1346,16 +1346,16 @@ export default function WikiSourcesPanel() {
|
|||||||
{showAddDoc && (
|
{showAddDoc && (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
caption={`添加文档到 ${wikiName}`}
|
caption={`Add document to ${wikiName}`}
|
||||||
size="m"
|
size="m"
|
||||||
onClose={() => setShowAddDoc(false)}
|
onClose={() => setShowAddDoc(false)}
|
||||||
disableEscape={submitting}
|
disableEscape={submitting}
|
||||||
>
|
>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Alert type="info">选择方式导入文档</Alert>
|
<Alert type="info">Choose a way to import documents</Alert>
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ id: 'file', label: '上传文件' },
|
{ id: 'file', label: 'Upload file' },
|
||||||
{ id: 'markdown', label: 'Markdown' },
|
{ id: 'markdown', label: 'Markdown' },
|
||||||
]}
|
]}
|
||||||
activeId={addDocTab}
|
activeId={addDocTab}
|
||||||
@ -1374,13 +1374,13 @@ export default function WikiSourcesPanel() {
|
|||||||
const rejected = all.length - allowed.length;
|
const rejected = all.length - allowed.length;
|
||||||
if (rejected > 0) {
|
if (rejected > 0) {
|
||||||
tea.notify.warning(
|
tea.notify.warning(
|
||||||
`已忽略 ${rejected} 个非 Markdown 文件(仅支持 .md/.txt/.markdown)`,
|
`Ignored ${rejected} non-Markdown file(s) (.md/.txt/.markdown only)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (allowed.length > 0) setPendingFiles((prev) => [...prev, ...allowed]);
|
if (allowed.length > 0) setPendingFiles((prev) => [...prev, ...allowed]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text theme="weak">拖拽或点击选择 Markdown 文件(可多选)</Text>
|
<Text theme="weak">Drag and drop or click to select Markdown files (multiple allowed)</Text>
|
||||||
</div>
|
</div>
|
||||||
{pendingFiles.length > 0 && (
|
{pendingFiles.length > 0 && (
|
||||||
<div className="_wiki-detail-upload-files">
|
<div className="_wiki-detail-upload-files">
|
||||||
@ -1410,7 +1410,7 @@ export default function WikiSourcesPanel() {
|
|||||||
setPendingFiles((prev) => prev.filter((_, j) => j !== i))
|
setPendingFiles((prev) => prev.filter((_, j) => j !== i))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
删除
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1419,14 +1419,14 @@ export default function WikiSourcesPanel() {
|
|||||||
)}
|
)}
|
||||||
{pendingFiles.length > 0 && (
|
{pendingFiles.length > 0 && (
|
||||||
<div className="_wiki-detail-upload-footer">
|
<div className="_wiki-detail-upload-footer">
|
||||||
<Text theme="weak">{pendingFiles.length} 个文件待上传</Text>
|
<Text theme="weak">{pendingFiles.length} file(s) pending upload</Text>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleBatchUpload}
|
onClick={handleBatchUpload}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? '上传中…' : '确认上传'}
|
{submitting ? 'Uploading…' : 'Confirm upload'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1453,7 +1453,7 @@ export default function WikiSourcesPanel() {
|
|||||||
type="text"
|
type="text"
|
||||||
onClick={() => setMdDocs((prev) => prev.filter((_, j) => j !== i))}
|
onClick={() => setMdDocs((prev) => prev.filter((_, j) => j !== i))}
|
||||||
>
|
>
|
||||||
删除
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1466,19 +1466,19 @@ export default function WikiSourcesPanel() {
|
|||||||
prev.map((d, j) => (j === i ? { ...d, content: v } : d)),
|
prev.map((d, j) => (j === i ? { ...d, content: v } : d)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder="# 标题"
|
placeholder="# Title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setMdDocs((prev) => [...prev, { filename: '', content: '' }])}
|
onClick={() => setMdDocs((prev) => [...prev, { filename: '', content: '' }])}
|
||||||
>
|
>
|
||||||
+ 添加一条
|
+ Add one
|
||||||
</Button>
|
</Button>
|
||||||
<div className="_wiki-detail-upload-footer">
|
<div className="_wiki-detail-upload-footer">
|
||||||
<Text theme="weak">
|
<Text theme="weak">
|
||||||
{mdDocs.filter((d) => d.filename.trim() && d.content.trim()).length}{' '}
|
{mdDocs.filter((d) => d.filename.trim() && d.content.trim()).length}{' '}
|
||||||
个待上传
|
pending upload
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -1488,7 +1488,7 @@ export default function WikiSourcesPanel() {
|
|||||||
}
|
}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? '上传中…' : '确认上传'}
|
{submitting ? 'Uploading…' : 'Confirm upload'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1508,7 +1508,7 @@ export default function WikiSourcesPanel() {
|
|||||||
const rejected = all.length - allowed.length;
|
const rejected = all.length - allowed.length;
|
||||||
if (rejected > 0) {
|
if (rejected > 0) {
|
||||||
tea.notify.warning(
|
tea.notify.warning(
|
||||||
`已忽略 ${rejected} 个非 Markdown 文件(仅支持 .md/.txt/.markdown)`,
|
`Ignored ${rejected} non-Markdown file(s) (.md/.txt/.markdown only)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (allowed.length > 0) setPendingFiles((prev) => [...prev, ...allowed]);
|
if (allowed.length > 0) setPendingFiles((prev) => [...prev, ...allowed]);
|
||||||
@ -1528,12 +1528,12 @@ export default function WikiSourcesPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="_asset-wiki-page">
|
<div className="_asset-wiki-page">
|
||||||
<AssetPageHeader
|
<AssetPageHeader
|
||||||
title="Wiki 知识库"
|
title="Wiki knowledge base"
|
||||||
subtitle={
|
subtitle={
|
||||||
<Text theme="label">
|
<Text theme="label">
|
||||||
{activeTeam
|
{activeTeam
|
||||||
? `${activeTeam.name} · 共 ${stats.total} 个知识库`
|
? `${activeTeam.name} · ${stats.total} knowledge bases total`
|
||||||
: `共 ${stats.total} 个知识库`}
|
: `${stats.total} knowledge bases total`}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
scope={
|
scope={
|
||||||
@ -1554,10 +1554,10 @@ export default function WikiSourcesPanel() {
|
|||||||
value={agentFilter}
|
value={agentFilter}
|
||||||
onChange={setAgentFilter}
|
onChange={setAgentFilter}
|
||||||
disabled={teamAgents.length === 0}
|
disabled={teamAgents.length === 0}
|
||||||
placeholder="无可选 Agent"
|
placeholder="No agent available"
|
||||||
options={teamAgents.map((agent) => ({
|
options={teamAgents.map((agent) => ({
|
||||||
value: agent.id,
|
value: agent.id,
|
||||||
text: `${agent.name}(${agent.id})`,
|
text: `${agent.name} (${agent.id})`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
@ -1567,16 +1567,16 @@ export default function WikiSourcesPanel() {
|
|||||||
<Card className="_asset-wiki-content-card">
|
<Card className="_asset-wiki-content-card">
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<div className="_asset-wiki-stats">
|
<div className="_asset-wiki-stats">
|
||||||
<MetricsBoard title="知识库总数" value={stats.total} />
|
<MetricsBoard title="Total knowledge bases" value={stats.total} />
|
||||||
<MetricsBoard title="已就绪" value={stats.ready} />
|
<MetricsBoard title="Ready" value={stats.ready} />
|
||||||
<MetricsBoard title="处理中" value={stats.processing} />
|
<MetricsBoard title="Processing" value={stats.processing} />
|
||||||
<MetricsBoard title="总页面数" value={stats.totalPages} />
|
<MetricsBoard title="Total pages" value={stats.totalPages} />
|
||||||
</div>
|
</div>
|
||||||
<Table.ActionPanel>
|
<Table.ActionPanel>
|
||||||
<Justify
|
<Justify
|
||||||
left={
|
left={
|
||||||
<Button type="primary" onClick={() => setShowCreate(true)}>
|
<Button type="primary" onClick={() => setShowCreate(true)}>
|
||||||
+ 新建 Wiki
|
+ New wiki
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
@ -1584,15 +1584,15 @@ export default function WikiSourcesPanel() {
|
|||||||
<SearchBox
|
<SearchBox
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={setKeyword}
|
onChange={setKeyword}
|
||||||
placeholder="搜索名称 / ID"
|
placeholder="Search name / ID"
|
||||||
/>
|
/>
|
||||||
<Segment
|
<Segment
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
onChange={(value) => setStatusFilter(value as StatusFilter)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', text: '全部状态' },
|
{ value: 'all', text: 'All statuses' },
|
||||||
{ value: 'ready', text: '就绪' },
|
{ value: 'ready', text: 'Ready' },
|
||||||
{ value: 'processing', text: '处理中' },
|
{ value: 'processing', text: 'Processing' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Segment
|
<Segment
|
||||||
@ -1616,13 +1616,13 @@ export default function WikiSourcesPanel() {
|
|||||||
emptyText={
|
emptyText={
|
||||||
<div className="_asset-wiki-empty">
|
<div className="_asset-wiki-empty">
|
||||||
<BooksIcon size="large" />
|
<BooksIcon size="large" />
|
||||||
<Text>暂无 Wiki 知识库</Text>
|
<Text>No wiki knowledge bases</Text>
|
||||||
<Text theme="label">点击上方“+ 新建 Wiki”创建第一个</Text>
|
<Text theme="label">Click "+ New wiki" above to create your first one</Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : filteredSources.length === 0 ? (
|
) : filteredSources.length === 0 ? (
|
||||||
<StatusTip status="empty" emptyText="没有匹配的 Wiki,试试调整搜索或筛选条件。" />
|
<StatusTip status="empty" emptyText="No matching wikis. Try adjusting your search or filters." />
|
||||||
) : viewMode === 'card' ? (
|
) : viewMode === 'card' ? (
|
||||||
<div className="_asset-wiki-grid">
|
<div className="_asset-wiki-grid">
|
||||||
{filteredSources.map((source) => (
|
{filteredSources.map((source) => (
|
||||||
@ -1641,20 +1641,20 @@ export default function WikiSourcesPanel() {
|
|||||||
<div className="_asset-wiki-card-meta">
|
<div className="_asset-wiki-card-meta">
|
||||||
<WikiStatusBadge status={source.status} />
|
<WikiStatusBadge status={source.status} />
|
||||||
<span>
|
<span>
|
||||||
{source.page_count ?? 0} 页 · {formatShortTime(source.last_sync_at)}
|
{source.page_count ?? 0} pages · {formatShortTime(source.last_sync_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="_asset-wiki-card-owner">
|
<div className="_asset-wiki-card-owner">
|
||||||
<UsergroupIcon size={12} />
|
<UsergroupIcon size={12} />
|
||||||
{scopeTab === 'fixed' ? (
|
{scopeTab === 'fixed' ? (
|
||||||
`固定资产 · ${agentFilter || '未选择 Agent'}`
|
`Fixed asset · ${agentFilter || 'No agent selected'}`
|
||||||
) : source.owner_user_id ? (
|
) : source.owner_user_id ? (
|
||||||
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
||||||
) : (
|
) : (
|
||||||
'团队 Wiki 池'
|
'Team wiki pool'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="_asset-wiki-card-id">ID:{source.wiki_id}</div>
|
<div className="_asset-wiki-card-id">ID: {source.wiki_id}</div>
|
||||||
<WikiActions
|
<WikiActions
|
||||||
source={source}
|
source={source}
|
||||||
scopeTab={scopeTab}
|
scopeTab={scopeTab}
|
||||||
@ -1676,7 +1676,7 @@ export default function WikiSourcesPanel() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
header: '名称',
|
header: 'Name',
|
||||||
width: 240,
|
width: 240,
|
||||||
render: (source) => (
|
render: (source) => (
|
||||||
<button
|
<button
|
||||||
@ -1692,35 +1692,35 @@ export default function WikiSourcesPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
header: '状态',
|
header: 'Status',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (source) => <WikiStatusBadge status={source.status} />,
|
render: (source) => <WikiStatusBadge status={source.status} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'page_count',
|
key: 'page_count',
|
||||||
header: '页数',
|
header: 'Pages',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (source) => source.page_count ?? 0,
|
render: (source) => source.page_count ?? 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'owner',
|
key: 'owner',
|
||||||
header: '归属',
|
header: 'Owner',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (source) =>
|
render: (source) =>
|
||||||
scopeTab === 'fixed' ? (
|
scopeTab === 'fixed' ? (
|
||||||
<span className="_asset-wiki-inline-icon">
|
<span className="_asset-wiki-inline-icon">
|
||||||
<UsergroupIcon size={12} />
|
<UsergroupIcon size={12} />
|
||||||
{agentFilter || '未选择 Agent'}
|
{agentFilter || 'No agent selected'}
|
||||||
</span>
|
</span>
|
||||||
) : source.owner_user_id ? (
|
) : source.owner_user_id ? (
|
||||||
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
|
||||||
) : (
|
) : (
|
||||||
<Text theme="label">团队池</Text>
|
<Text theme="label">Team pool</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'last_sync_at',
|
key: 'last_sync_at',
|
||||||
header: '最后更新时间',
|
header: 'Last updated',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (source) => (
|
render: (source) => (
|
||||||
<Text theme="label">{formatShortTime(source.last_sync_at)}</Text>
|
<Text theme="label">{formatShortTime(source.last_sync_at)}</Text>
|
||||||
@ -1734,7 +1734,7 @@ export default function WikiSourcesPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '操作',
|
header: 'Actions',
|
||||||
width: 240,
|
width: 240,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (source) => (
|
render: (source) => (
|
||||||
@ -1760,19 +1760,19 @@ export default function WikiSourcesPanel() {
|
|||||||
{showCreate && (
|
{showCreate && (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
caption="新建 Wiki"
|
caption="New wiki"
|
||||||
size="s"
|
size="s"
|
||||||
onClose={() => setShowCreate(false)}
|
onClose={() => setShowCreate(false)}
|
||||||
disableEscape={submitting}
|
disableEscape={submitting}
|
||||||
>
|
>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="名称" required extra="创建一个新的文档知识库">
|
<Form.Item label="Name" required extra="Create a new document knowledge base">
|
||||||
<Input
|
<Input
|
||||||
size="full"
|
size="full"
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={setNewName}
|
onChange={setNewName}
|
||||||
placeholder="如 team-docs"
|
placeholder="e.g. team-docs"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
@ -1784,10 +1784,10 @@ export default function WikiSourcesPanel() {
|
|||||||
disabled={submitting || !newName.trim()}
|
disabled={submitting || !newName.trim()}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? '创建中…' : '创建'}
|
{submitting ? 'Creating…' : 'Create'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setShowCreate(false)} disabled={submitting}>
|
<Button onClick={() => setShowCreate(false)} disabled={submitting}>
|
||||||
取消
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -1802,9 +1802,9 @@ export default function WikiSourcesPanel() {
|
|||||||
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
|
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
|
||||||
onClose={() => setAllocateTarget(null)}
|
onClose={() => setAllocateTarget(null)}
|
||||||
onAllocate={async (agentId) => {
|
onAllocate={async (agentId) => {
|
||||||
if (!activeTeamId) throw new Error('请先选择 team');
|
if (!activeTeamId) throw new Error('Select a team first');
|
||||||
await knowledgeApi.wiki.allocate(activeTeamId, allocateTarget.wiki_id, agentId);
|
await knowledgeApi.wiki.allocate(activeTeamId, allocateTarget.wiki_id, agentId);
|
||||||
tea.notify.success('已分配到 Agent');
|
tea.notify.success('Assigned to agent');
|
||||||
await fetchSources();
|
await fetchSources();
|
||||||
if (scopeTab === 'fixed') await fetchFixedBindings();
|
if (scopeTab === 'fixed') await fetchFixedBindings();
|
||||||
}}
|
}}
|
||||||
@ -1837,11 +1837,11 @@ function WikiActions({
|
|||||||
return (
|
return (
|
||||||
<div className="_asset-wiki-actions" onClick={(event) => event.stopPropagation()}>
|
<div className="_asset-wiki-actions" onClick={(event) => event.stopPropagation()}>
|
||||||
<Button type="weak" disabled={ingestBusy} onClick={() => onIngest(source.wiki_id)}>
|
<Button type="weak" disabled={ingestBusy} onClick={() => onIngest(source.wiki_id)}>
|
||||||
<StarIcon size={14} /> {isCurrentIngesting ? 'Ingest 中…' : ingestBusy ? '排队中…' : 'Ingest'}
|
<StarIcon size={14} /> {isCurrentIngesting ? 'Ingesting…' : ingestBusy ? 'Queued…' : 'Ingest'}
|
||||||
</Button>
|
</Button>
|
||||||
{scopeTab === 'fixed' ? (
|
{scopeTab === 'fixed' ? (
|
||||||
<Button type="weak" onClick={() => onUnbind(source.wiki_id)}>
|
<Button type="weak" onClick={() => onUnbind(source.wiki_id)}>
|
||||||
解绑
|
Unbind
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -1850,16 +1850,16 @@ function WikiActions({
|
|||||||
tooltip={
|
tooltip={
|
||||||
source.status === 'ready'
|
source.status === 'ready'
|
||||||
? undefined
|
? undefined
|
||||||
: '该 Wiki 尚未加工完成(未 ready),暂不能分配到 Agent'
|
: 'This wiki has not finished processing (not ready) and cannot be assigned to an agent yet'
|
||||||
}
|
}
|
||||||
onClick={() => onAllocate({ wiki_id: source.wiki_id, name: source.name })}
|
onClick={() => onAllocate({ wiki_id: source.wiki_id, name: source.name })}
|
||||||
>
|
>
|
||||||
分配
|
Assign
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="icon"
|
type="icon"
|
||||||
tooltip="删除"
|
tooltip="Delete"
|
||||||
onClick={() => onDelete(source.wiki_id, source.name)}
|
onClick={() => onDelete(source.wiki_id, source.name)}
|
||||||
>
|
>
|
||||||
<DeleteIcon size={14} />
|
<DeleteIcon size={14} />
|
||||||
@ -1951,7 +1951,7 @@ function GraphTabContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className="_wiki-detail-side-empty">
|
<div className="_wiki-detail-side-empty">
|
||||||
<ArchitectureIcon size="large" />
|
<ArchitectureIcon size="large" />
|
||||||
<Text theme="label">点击节点查看内容</Text>
|
<Text theme="label">Click a node to view its content</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -2010,7 +2010,7 @@ function PagesTabContent({
|
|||||||
className={`_wiki-detail-filter-tag${pageTypeFilter === 'all' ? ' is-active' : ''}`}
|
className={`_wiki-detail-filter-tag${pageTypeFilter === 'all' ? ' is-active' : ''}`}
|
||||||
onClick={() => setPageTypeFilter('all')}
|
onClick={() => setPageTypeFilter('all')}
|
||||||
>
|
>
|
||||||
全部 {allPages.length}
|
All {allPages.length}
|
||||||
</button>
|
</button>
|
||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<button
|
<button
|
||||||
@ -2047,9 +2047,9 @@ function PagesTabContent({
|
|||||||
type="text"
|
type="text"
|
||||||
className="_wiki-detail-page-delete"
|
className="_wiki-detail-page-delete"
|
||||||
onClick={() => onDeletePage(page)}
|
onClick={() => onDeletePage(page)}
|
||||||
tooltip="删除页面"
|
tooltip="Delete page"
|
||||||
>
|
>
|
||||||
删除
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -2087,7 +2087,7 @@ function PagesTabContent({
|
|||||||
{tag.trim()}
|
{tag.trim()}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
{metadata.created && <Text theme="label">创建:{metadata.created}</Text>}
|
{metadata.created && <Text theme="label">Created: {metadata.created}</Text>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{readLoading ? (
|
{readLoading ? (
|
||||||
@ -2105,7 +2105,7 @@ function PagesTabContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className="_wiki-detail-side-empty">
|
<div className="_wiki-detail-side-empty">
|
||||||
<BooksIcon size="large" />
|
<BooksIcon size="large" />
|
||||||
<Text theme="label">选择左侧页面查看内容</Text>
|
<Text theme="label">Select a page on the left to view its content</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -2140,7 +2140,7 @@ function RawFilesSection({
|
|||||||
knowledgeApi.wiki
|
knowledgeApi.wiki
|
||||||
.rawList(wikiId)
|
.rawList(wikiId)
|
||||||
.then((r: any) => setFiles(r?.files || []))
|
.then((r: any) => setFiles(r?.files || []))
|
||||||
.catch((e: any) => tea.notify.error(e?.message || '加载原始文档列表失败'))
|
.catch((e: any) => tea.notify.error(e?.message || 'Failed to load original document list'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [wikiId]);
|
}, [wikiId]);
|
||||||
|
|
||||||
@ -2158,7 +2158,7 @@ function RawFilesSection({
|
|||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
<div className="_wiki-detail-rawfiles-loading">
|
<div className="_wiki-detail-rawfiles-loading">
|
||||||
<FolderIcon size={12} /> 原始文档加载中…
|
<FolderIcon size={12} /> Loading original documents…
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (files.length === 0) return null;
|
if (files.length === 0) return null;
|
||||||
@ -2167,7 +2167,7 @@ function RawFilesSection({
|
|||||||
<div className="_wiki-detail-rawfiles">
|
<div className="_wiki-detail-rawfiles">
|
||||||
<button className="_wiki-detail-rawfiles-toggle" onClick={() => setExpanded(!expanded)}>
|
<button className="_wiki-detail-rawfiles-toggle" onClick={() => setExpanded(!expanded)}>
|
||||||
<span>
|
<span>
|
||||||
<FolderIcon size={12} /> 原始文档({files.length})
|
<FolderIcon size={12} /> Original documents ({files.length})
|
||||||
</span>
|
</span>
|
||||||
<ChevronRightIcon size={12} className={expanded ? 'is-open' : ''} />
|
<ChevronRightIcon size={12} className={expanded ? 'is-open' : ''} />
|
||||||
</button>
|
</button>
|
||||||
@ -2184,9 +2184,9 @@ function RawFilesSection({
|
|||||||
type="text"
|
type="text"
|
||||||
className="_wiki-detail-page-delete"
|
className="_wiki-detail-page-delete"
|
||||||
onClick={() => void handleDelete(file.filename)}
|
onClick={() => void handleDelete(file.filename)}
|
||||||
tooltip="删除原始文档"
|
tooltip="Delete original document"
|
||||||
>
|
>
|
||||||
删除
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -2213,7 +2213,7 @@ function KnowledgeGraphEmbed({
|
|||||||
highlightNode: string | null;
|
highlightNode: string | null;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<StatusTip status="loading" loadingText="加载图谱组件…" />}>
|
<Suspense fallback={<StatusTip status="loading" loadingText="Loading graph component…" />}>
|
||||||
<KnowledgeGraphLazy
|
<KnowledgeGraphLazy
|
||||||
data={data}
|
data={data}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@ -69,14 +69,14 @@ export default function TaskCreateDialog(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible caption="新建 Task" size="m" onClose={props.onClose} disableEscape={submitting}>
|
<Modal visible caption="New task" size="m" onClose={props.onClose} disableEscape={submitting}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item label="所属 Team">
|
<Form.Item label="Team">
|
||||||
<div className="_memory-tcd-team-row">
|
<div className="_memory-tcd-team-row">
|
||||||
<span className="_memory-tcd-team-avatar">{props.team.name.slice(0, 1).toUpperCase()}</span>
|
<span className="_memory-tcd-team-avatar">{props.team.name.slice(0, 1).toUpperCase()}</span>
|
||||||
<div className="_memory-tcd-team-meta">
|
<div className="_memory-tcd-team-meta">
|
||||||
<div className="_memory-tcd-team-label">将创建到 team</div>
|
<div className="_memory-tcd-team-label">Will be created in team</div>
|
||||||
<div className="_memory-tcd-team-name-row">
|
<div className="_memory-tcd-team-name-row">
|
||||||
<span className="_memory-tcd-team-name">{props.team.name}</span>
|
<span className="_memory-tcd-team-name">{props.team.name}</span>
|
||||||
<Tag size="sm">{props.team.team_id}</Tag>
|
<Tag size="sm">{props.team.team_id}</Tag>
|
||||||
@ -84,30 +84,30 @@ export default function TaskCreateDialog(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="标题" required>
|
<Form.Item label="Title" required>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
size="full"
|
size="full"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={setTitle}
|
onChange={setTitle}
|
||||||
placeholder="例如:修复 #142 macOS 14 启动失败"
|
placeholder="e.g. Fix #142 macOS 14 startup failure"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="描述" required extra="关联 Agent 可在创建后再挂载">
|
<Form.Item label="Description" required extra="Linked agents can be attached after creation">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
size="full"
|
size="full"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={setDescription}
|
onChange={setDescription}
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="包含背景、目标、验收标准。建议越具体越好,方便 agent 理解上下文。"
|
placeholder="Include background, goals, and acceptance criteria. The more specific, the better — it helps the agent understand the context."
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button type="primary" onClick={() => void submit()} disabled={!canSubmit} loading={submitting}>创建 Task</Button>
|
<Button type="primary" onClick={() => void submit()} disabled={!canSubmit} loading={submitting}>Create task</Button>
|
||||||
<Button onClick={props.onClose} disabled={submitting}>取消</Button>
|
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -44,8 +44,8 @@ function errMsg(e: unknown): string {
|
|||||||
// 历史的 待处理 / 阻塞 / 已归档 已下线(参见 backendStore.ts 里的 normalizeTaskStatus)。
|
// 历史的 待处理 / 阻塞 / 已归档 已下线(参见 backendStore.ts 里的 normalizeTaskStatus)。
|
||||||
|
|
||||||
const STATUS_LABEL: Record<Task['status'], string> = {
|
const STATUS_LABEL: Record<Task['status'], string> = {
|
||||||
running: '进行中',
|
running: 'In progress',
|
||||||
completed: '已完成'
|
completed: 'Completed'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tag 组件合法 theme: default/primary/success/warning/error
|
// Tag 组件合法 theme: default/primary/success/warning/error
|
||||||
@ -172,7 +172,7 @@ export default function TaskWorkbench(props: {
|
|||||||
// 谁点击「创建 Task」,谁就是 creator_user_id。
|
// 谁点击「创建 Task」,谁就是 creator_user_id。
|
||||||
const team = teams.find((t) => t.team_id === draft.team_id);
|
const team = teams.find((t) => t.team_id === draft.team_id);
|
||||||
if (!team) {
|
if (!team) {
|
||||||
tea.notify.error(`team「${draft.team_id}」不存在,无法创建 task。`);
|
tea.notify.error(`Team "${draft.team_id}" does not exist. Cannot create task.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -207,15 +207,15 @@ export default function TaskWorkbench(props: {
|
|||||||
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
||||||
if (!canDeleteTask(task, team, currentUser) && !isAdmin) {
|
if (!canDeleteTask(task, team, currentUser) && !isAdmin) {
|
||||||
tea.notify.warning(
|
tea.notify.warning(
|
||||||
`你不是 task「${task.title}」的创建者,也不是 team 管理员,无法删除。创建者: ${task.creator_user_id}`
|
`You are not the creator of task "${task.title}" nor a team admin, so you cannot delete it. Creator: ${task.creator_user_id}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ok = await tea.confirm({
|
const ok = await tea.confirm({
|
||||||
message: `确认删除 task「${task.title}」?`,
|
message: `Delete task "${task.title}"?`,
|
||||||
description: `Task ID: ${task.task_id}`,
|
description: `Task ID: ${task.task_id}`,
|
||||||
okText: '删除',
|
okText: 'Delete',
|
||||||
cancelText: '取消',
|
cancelText: 'Cancel',
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
try {
|
try {
|
||||||
@ -230,7 +230,7 @@ export default function TaskWorkbench(props: {
|
|||||||
// 权限:编辑 task(含切换 status)允许 team 内任意 member / admin
|
// 权限:编辑 task(含切换 status)允许 team 内任意 member / admin
|
||||||
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
||||||
if (!canEditTask(task, team, currentUser) && !isAdmin) {
|
if (!canEditTask(task, team, currentUser) && !isAdmin) {
|
||||||
tea.notify.warning('你不是该 team 的成员,无权修改此 task。');
|
tea.notify.warning('You are not a member of this team and cannot modify this task.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -242,7 +242,7 @@ export default function TaskWorkbench(props: {
|
|||||||
onUpdateTask={async (task, patch) => {
|
onUpdateTask={async (task, patch) => {
|
||||||
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
|
||||||
if (!canEditTask(task, team, currentUser) && !isAdmin) {
|
if (!canEditTask(task, team, currentUser) && !isAdmin) {
|
||||||
tea.notify.warning('你不是该 team 的成员,无权修改此 task。');
|
tea.notify.warning('You are not a member of this team and cannot modify this task.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -278,9 +278,9 @@ function EmptyTeam() {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Body className="_memory-workbench-empty-card">
|
<Card.Body className="_memory-workbench-empty-card">
|
||||||
<Text theme="strong" className="_memory-workbench-empty-title">还没有可用的 Team</Text>
|
<Text theme="strong" className="_memory-workbench-empty-title">No team available yet</Text>
|
||||||
<Text theme="weak" className="_memory-workbench-empty-desc">
|
<Text theme="weak" className="_memory-workbench-empty-desc">
|
||||||
请先在「团队管理」里创建一个 team,再回到工作台创建 task。
|
Create a team in "Team management" first, then come back to the workbench to create a task.
|
||||||
</Text>
|
</Text>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card>
|
</Card>
|
||||||
@ -321,15 +321,15 @@ function BoardView({
|
|||||||
<Card className="_memory-workbench-card">
|
<Card className="_memory-workbench-card">
|
||||||
<Card.Body className="_memory-workbench-list-body">
|
<Card.Body className="_memory-workbench-list-body">
|
||||||
<div className="_memory-workbench-list-header">
|
<div className="_memory-workbench-list-header">
|
||||||
<Text theme="strong">Task 列表</Text>
|
<Text theme="strong">Task list</Text>
|
||||||
<Button type="primary" onClick={onCreate}>
|
<Button type="primary" onClick={onCreate}>
|
||||||
<AddIcon size={14} />
|
<AddIcon size={14} />
|
||||||
新建 Task
|
New task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tasks.length === 0 ? (
|
{tasks.length === 0 ? (
|
||||||
<div className="_memory-workbench-list-empty">
|
<div className="_memory-workbench-list-empty">
|
||||||
<Text theme="weak">暂无 task。点击右上角「新建 Task」创建第一个。</Text>
|
<Text theme="weak">No tasks yet. Click "New task" in the top right to create the first one.</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<List split="divide" className="_memory-workbench-list-items">
|
<List split="divide" className="_memory-workbench-list-items">
|
||||||
@ -361,7 +361,7 @@ function BoardView({
|
|||||||
</Text>
|
</Text>
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<Button type="text"
|
<Button type="text"
|
||||||
tooltip="删除该 Task"
|
tooltip="Delete this task"
|
||||||
className="_memory-workbench-item-delete"
|
className="_memory-workbench-item-delete"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
@ -379,16 +379,16 @@ function BoardView({
|
|||||||
session 的 user 集合;hover 看完整 user_id 列表 */}
|
session 的 user 集合;hover 看完整 user_id 列表 */}
|
||||||
<span
|
<span
|
||||||
className="_memory-workbench-badge"
|
className="_memory-workbench-badge"
|
||||||
title={view.users.length === 0 ? '暂无实际参与的 user' : `实际参与 User:\n${view.users.join('\n')}`}
|
title={view.users.length === 0 ? 'No users have actually participated yet' : `Actual participating users:\n${view.users.join('\n')}`}
|
||||||
>
|
>
|
||||||
<UsergroupIcon size={12} />
|
<UsergroupIcon size={12} />
|
||||||
{view.users.length} 人
|
{view.users.length} people
|
||||||
{imParticipant && <Tag theme="warning" variant="soft" size="sm">含你</Tag>}
|
{imParticipant && <Tag theme="warning" variant="soft" size="sm">Includes you</Tag>}
|
||||||
</span>
|
</span>
|
||||||
{/* agent 数字同源 —— 实际起过 session 的 agent 去重后个数;
|
{/* agent 数字同源 —— 实际起过 session 的 agent 去重后个数;
|
||||||
hover 看完整 agent name / agent_id 兜底 */}
|
hover 看完整 agent name / agent_id 兜底 */}
|
||||||
<span title={agentLabels.length === 0 ? '暂无实际参与的 Agent' : `实际参与 Agent:\n${agentLabels.join('\n')}`}>
|
<span title={agentLabels.length === 0 ? 'No agents have actually participated yet' : `Actual participating agents:\n${agentLabels.join('\n')}`}>
|
||||||
{agentLabels.length} 个 Agent
|
{agentLabels.length} agents
|
||||||
</span>
|
</span>
|
||||||
<span className="_memory-workbench-item-time">
|
<span className="_memory-workbench-item-time">
|
||||||
{new Date(t.updated_at_ms).toLocaleString()}
|
{new Date(t.updated_at_ms).toLocaleString()}
|
||||||
@ -409,7 +409,7 @@ function BoardView({
|
|||||||
<Card.Body className="_memory-workbench-detail-body">
|
<Card.Body className="_memory-workbench-detail-body">
|
||||||
{!selected ? (
|
{!selected ? (
|
||||||
<div className="_memory-workbench-detail-empty">
|
<div className="_memory-workbench-detail-empty">
|
||||||
<Text theme="weak">在左侧选中一条 task,或点击「新建 Task」开始一个新任务。</Text>
|
<Text theme="weak">Select a task on the left, or click "New task" to start a new one.</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TaskDetail
|
<TaskDetail
|
||||||
@ -478,7 +478,7 @@ function TaskDetail({
|
|||||||
const patch: Partial<Pick<Task, 'title' | 'description' | 'source_type' | 'source_url' | 'linked_agents'>> = {};
|
const patch: Partial<Pick<Task, 'title' | 'description' | 'source_type' | 'source_url' | 'linked_agents'>> = {};
|
||||||
const title = draftTitle.trim();
|
const title = draftTitle.trim();
|
||||||
if (title.length === 0) {
|
if (title.length === 0) {
|
||||||
tea.notify.warning('任务标题不能为空。');
|
tea.notify.warning('Task title cannot be empty.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (title !== task.title) patch.title = title;
|
if (title !== task.title) patch.title = title;
|
||||||
@ -514,7 +514,7 @@ function TaskDetail({
|
|||||||
<Input
|
<Input
|
||||||
value={draftTitle}
|
value={draftTitle}
|
||||||
onChange={setDraftTitle}
|
onChange={setDraftTitle}
|
||||||
placeholder="任务标题"
|
placeholder="Task title"
|
||||||
size="full"
|
size="full"
|
||||||
className="_memory-workbench-title-input"
|
className="_memory-workbench-title-input"
|
||||||
/>
|
/>
|
||||||
@ -522,35 +522,35 @@ function TaskDetail({
|
|||||||
<Text theme="strong" className="_memory-workbench-detail-title">{task.title}</Text>
|
<Text theme="strong" className="_memory-workbench-detail-title">{task.title}</Text>
|
||||||
)}
|
)}
|
||||||
<div className="_memory-workbench-detail-meta">
|
<div className="_memory-workbench-detail-meta">
|
||||||
<Text theme="weak">task_id:</Text>
|
<Text theme="weak">task_id:</Text>
|
||||||
<Text theme="text" className="_memory-mono">{task.task_id}</Text>
|
<Text theme="text" className="_memory-mono">{task.task_id}</Text>
|
||||||
<span className="_memory-workbench-meta-sep">·</span>
|
<span className="_memory-workbench-meta-sep">·</span>
|
||||||
<Text theme="weak">team:</Text>
|
<Text theme="weak">team:</Text>
|
||||||
{team ? (
|
{team ? (
|
||||||
<>
|
<>
|
||||||
<Text theme="text">{team.name}</Text>
|
<Text theme="text">{team.name}</Text>
|
||||||
<Text theme="weak" className="_memory-mono">({team.team_id})</Text>
|
<Text theme="weak" className="_memory-mono">({team.team_id})</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text theme="weak" className="_memory-mono">{task.team_id} · 已删除</Text>
|
<Text theme="weak" className="_memory-mono">{task.team_id} · Deleted</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-workbench-detail-actions">
|
<div className="_memory-workbench-detail-actions">
|
||||||
{/* 编辑按钮:仅 team 成员可见可点;编辑态下隐藏,由保存/取消替代 */}
|
{/* 编辑按钮:仅 team 成员可见可点;编辑态下隐藏,由保存/取消替代 */}
|
||||||
{!editing && canEdit && (
|
{!editing && canEdit && (
|
||||||
<Button onClick={startEdit} tooltip="编辑任务详情(标题、描述)">
|
<Button onClick={startEdit} tooltip="Edit task details (title, description)">
|
||||||
<EditIcon size={14} />
|
<EditIcon size={14} />
|
||||||
编辑
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{editing && (
|
{editing && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={cancelEdit}>取消</Button>
|
<Button onClick={cancelEdit}>Cancel</Button>
|
||||||
<Button type="primary" onClick={saveEdit}>保存</Button>
|
<Button type="primary" onClick={saveEdit}>Save</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="_memory-workbench-status-switch" title={canEdit ? '切换任务状态(你会被加入参与者)' : '仅 team 成员可切换 task 状态'}>
|
<div className="_memory-workbench-status-switch" title={canEdit ? 'Switch task status (you will be added as a participant)' : 'Only team members can switch task status'}>
|
||||||
{(Object.keys(STATUS_LABEL) as Task['status'][]).map((s) => {
|
{(Object.keys(STATUS_LABEL) as Task['status'][]).map((s) => {
|
||||||
const active = task.status === s;
|
const active = task.status === s;
|
||||||
return (
|
return (
|
||||||
@ -574,18 +574,18 @@ function TaskDetail({
|
|||||||
完成时 fire-and-forget append 到内核;append-only 语义,前端按 Set 去重。 */}
|
完成时 fire-and-forget append 到内核;append-only 语义,前端按 Set 去重。 */}
|
||||||
<div className="_memory-workbench-people">
|
<div className="_memory-workbench-people">
|
||||||
<div className="_memory-workbench-people-row">
|
<div className="_memory-workbench-people-row">
|
||||||
<Text theme="weak" className="_memory-workbench-people-label">创建者</Text>
|
<Text theme="weak" className="_memory-workbench-people-label">Creator</Text>
|
||||||
<span
|
<span
|
||||||
className="_memory-workbench-chip"
|
className="_memory-workbench-chip"
|
||||||
title="创建者(默认 = team admin);仅创建者或 team 管理员可删除本 task"
|
title="Creator (default = team admin); only the creator or a team admin can delete this task"
|
||||||
>
|
>
|
||||||
<UserIcon size={12} />
|
<UserIcon size={12} />
|
||||||
<Text theme="text">{task.creator_user_id}</Text>
|
<Text theme="text">{task.creator_user_id}</Text>
|
||||||
{task.creator_user_id === currentUser && <Tag theme="warning" variant="soft" size="sm">你</Tag>}
|
{task.creator_user_id === currentUser && <Tag theme="warning" variant="soft" size="sm">You</Tag>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-workbench-people-row">
|
<div className="_memory-workbench-people-row">
|
||||||
<Text theme="weak" className="_memory-workbench-people-label">参与的 User</Text>
|
<Text theme="weak" className="_memory-workbench-people-label">Participating users</Text>
|
||||||
{participantUsers.length === 0 ? (
|
{participantUsers.length === 0 ? (
|
||||||
<Text theme="weak">—</Text>
|
<Text theme="weak">—</Text>
|
||||||
) : (
|
) : (
|
||||||
@ -593,17 +593,17 @@ function TaskDetail({
|
|||||||
<span
|
<span
|
||||||
key={u}
|
key={u}
|
||||||
className="_memory-workbench-chip"
|
className="_memory-workbench-chip"
|
||||||
title="参与者:通过 proxy 起过 session 的 user(含 creator 自己开工)"
|
title="Participant: a user who has started a session via the proxy (including the creator's own work)"
|
||||||
>
|
>
|
||||||
<UserIcon size={12} />
|
<UserIcon size={12} />
|
||||||
<Text theme="text">{u}</Text>
|
<Text theme="text">{u}</Text>
|
||||||
{u === currentUser && <Tag theme="warning" variant="soft" size="sm">你</Tag>}
|
{u === currentUser && <Tag theme="warning" variant="soft" size="sm">You</Tag>}
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="_memory-workbench-people-row">
|
<div className="_memory-workbench-people-row">
|
||||||
<Text theme="weak" className="_memory-workbench-people-label">实际参与 Agent</Text>
|
<Text theme="weak" className="_memory-workbench-people-label">Actual participating agents</Text>
|
||||||
{sessionAgents.length === 0 ? (
|
{sessionAgents.length === 0 ? (
|
||||||
<Text theme="weak">—</Text>
|
<Text theme="weak">—</Text>
|
||||||
) : (
|
) : (
|
||||||
@ -611,7 +611,7 @@ function TaskDetail({
|
|||||||
<span
|
<span
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="_memory-workbench-chip"
|
className="_memory-workbench-chip"
|
||||||
title={`proxy 侧观测到起过 session 的 agent · agent_id=${a.id}`}
|
title={`Agent observed to have started a session on the proxy side · agent_id=${a.id}`}
|
||||||
>
|
>
|
||||||
<UsergroupIcon size={12} />
|
<UsergroupIcon size={12} />
|
||||||
<Text theme="text">{a.name}</Text>
|
<Text theme="text">{a.name}</Text>
|
||||||
@ -623,14 +623,14 @@ function TaskDetail({
|
|||||||
|
|
||||||
{/* === 描述 === */}
|
{/* === 描述 === */}
|
||||||
<div className="_memory-workbench-block">
|
<div className="_memory-workbench-block">
|
||||||
<Text theme="label" className="_memory-workbench-block-label">任务描述</Text>
|
<Text theme="label" className="_memory-workbench-block-label">Task description</Text>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
value={draftDesc}
|
value={draftDesc}
|
||||||
onChange={setDraftDesc}
|
onChange={setDraftDesc}
|
||||||
rows={6}
|
rows={6}
|
||||||
size="full"
|
size="full"
|
||||||
placeholder="包含背景、目标、验收标准…"
|
placeholder="Include background, goals, acceptance criteria…"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<pre className="_memory-workbench-desc-view">{task.description}</pre>
|
<pre className="_memory-workbench-desc-view">{task.description}</pre>
|
||||||
@ -641,7 +641,7 @@ function TaskDetail({
|
|||||||
task-agent/link 的人工声明关系不再在此页面展示。 */}
|
task-agent/link 的人工声明关系不再在此页面展示。 */}
|
||||||
|
|
||||||
<Text theme="weak" className="_memory-workbench-footer">
|
<Text theme="weak" className="_memory-workbench-footer">
|
||||||
创建:{new Date(task.created_at_ms).toLocaleString()} · 更新:{new Date(task.updated_at_ms).toLocaleString()}
|
Created: {new Date(task.created_at_ms).toLocaleString()} · Updated: {new Date(task.updated_at_ms).toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -90,11 +90,11 @@ export function findAccountByUsername(username: string): MockAccount | null {
|
|||||||
/** 校验邮箱 + 密码登录 */
|
/** 校验邮箱 + 密码登录 */
|
||||||
export function verifyAccountCredentials(email: string, password: string): MockAccount {
|
export function verifyAccountCredentials(email: string, password: string): MockAccount {
|
||||||
const e = email.trim().toLowerCase();
|
const e = email.trim().toLowerCase();
|
||||||
if (!e) throw new Error('请输入邮箱。');
|
if (!e) throw new Error('Please enter an email address.');
|
||||||
if (!password) throw new Error('请输入密码。');
|
if (!password) throw new Error('Please enter a password.');
|
||||||
const account = readAccounts().find((a) => a.email.toLowerCase() === e);
|
const account = readAccounts().find((a) => a.email.toLowerCase() === e);
|
||||||
if (!account) throw new Error(`账号不存在:${e}`);
|
if (!account) throw new Error(`Account not found: ${e}`);
|
||||||
if (account.password !== password) throw new Error('密码错误。');
|
if (account.password !== password) throw new Error('Incorrect password.');
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,11 +102,11 @@ export function verifyAccountCredentials(email: string, password: string): MockA
|
|||||||
* 用户名允许重复,邮箱全局唯一。 */
|
* 用户名允许重复,邮箱全局唯一。 */
|
||||||
export function createAccount(input: { email: string; username: string; password?: string; isAdmin?: boolean; description?: string }): MockAccount {
|
export function createAccount(input: { email: string; username: string; password?: string; isAdmin?: boolean; description?: string }): MockAccount {
|
||||||
const e = input.email.trim().toLowerCase();
|
const e = input.email.trim().toLowerCase();
|
||||||
if (!e) throw new Error('邮箱不能为空。');
|
if (!e) throw new Error('Email cannot be empty.');
|
||||||
if (!input.username.trim()) throw new Error('用户名不能为空。');
|
if (!input.username.trim()) throw new Error('Username cannot be empty.');
|
||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
if (accounts.some((a) => a.email.toLowerCase() === e)) {
|
if (accounts.some((a) => a.email.toLowerCase() === e)) {
|
||||||
throw new Error(`邮箱 "${input.email}" 已被注册。`);
|
throw new Error(`Email "${input.email}" is already registered.`);
|
||||||
}
|
}
|
||||||
const account: MockAccount = {
|
const account: MockAccount = {
|
||||||
email: input.email.trim(),
|
email: input.email.trim(),
|
||||||
@ -133,11 +133,11 @@ export function batchCreateAccounts(
|
|||||||
const e = entry.email.trim().toLowerCase();
|
const e = entry.email.trim().toLowerCase();
|
||||||
const u = entry.username.trim();
|
const u = entry.username.trim();
|
||||||
if (!e || !u) {
|
if (!e || !u) {
|
||||||
errors.push({ email: entry.email || '(空)', error: '邮箱和用户名都不能为空' });
|
errors.push({ email: entry.email || '(empty)', error: 'Email and username cannot be empty' });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (emailSet.has(e)) {
|
if (emailSet.has(e)) {
|
||||||
errors.push({ email: entry.email, error: '邮箱已被注册' });
|
errors.push({ email: entry.email, error: 'Email is already registered' });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const account: MockAccount = {
|
const account: MockAccount = {
|
||||||
@ -160,13 +160,13 @@ export function batchCreateAccounts(
|
|||||||
|
|
||||||
/** 修改密码 */
|
/** 修改密码 */
|
||||||
export function changePassword(username: string, oldPassword: string, newPassword: string): void {
|
export function changePassword(username: string, oldPassword: string, newPassword: string): void {
|
||||||
if (!oldPassword) throw new Error('请输入当前密码。');
|
if (!oldPassword) throw new Error('Please enter your current password.');
|
||||||
if (!newPassword) throw new Error('请输入新密码。');
|
if (!newPassword) throw new Error('Please enter a new password.');
|
||||||
if (newPassword.length < 4) throw new Error('新密码至少需要 4 位。');
|
if (newPassword.length < 4) throw new Error('New password must be at least 4 characters.');
|
||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
const account = accounts.find((a) => a.username === username);
|
const account = accounts.find((a) => a.username === username);
|
||||||
if (!account) throw new Error('账号不存在。');
|
if (!account) throw new Error('Account not found.');
|
||||||
if (account.password !== oldPassword) throw new Error('当前密码错误。');
|
if (account.password !== oldPassword) throw new Error('Current password is incorrect.');
|
||||||
account.password = newPassword;
|
account.password = newPassword;
|
||||||
writeAccountsRaw(accounts);
|
writeAccountsRaw(accounts);
|
||||||
}
|
}
|
||||||
@ -176,12 +176,12 @@ export function changePassword(username: string, oldPassword: string, newPasswor
|
|||||||
* 权限校验在 UI 层(仅 admin 可调用)。
|
* 权限校验在 UI 层(仅 admin 可调用)。
|
||||||
*/
|
*/
|
||||||
export function setAccountPassword(username: string, newPassword: string): void {
|
export function setAccountPassword(username: string, newPassword: string): void {
|
||||||
if (!newPassword) throw new Error('请输入新密码。');
|
if (!newPassword) throw new Error('Please enter a new password.');
|
||||||
if (newPassword.length < 4) throw new Error('新密码至少需要 4 位。');
|
if (newPassword.length < 4) throw new Error('New password must be at least 4 characters.');
|
||||||
if (!username) throw new Error('用户名不能为空。');
|
if (!username) throw new Error('Username cannot be empty.');
|
||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
const account = accounts.find((a) => a.username === username);
|
const account = accounts.find((a) => a.username === username);
|
||||||
if (!account) throw new Error(`账号不存在:${username}`);
|
if (!account) throw new Error(`Account not found: ${username}`);
|
||||||
account.password = newPassword;
|
account.password = newPassword;
|
||||||
writeAccountsRaw(accounts);
|
writeAccountsRaw(accounts);
|
||||||
}
|
}
|
||||||
@ -189,14 +189,14 @@ export function setAccountPassword(username: string, newPassword: string): void
|
|||||||
/** 修改用户邮箱(admin 专有权限,权限校验在 UI 层) */
|
/** 修改用户邮箱(admin 专有权限,权限校验在 UI 层) */
|
||||||
export function updateAccountEmail(username: string, newEmail: string): void {
|
export function updateAccountEmail(username: string, newEmail: string): void {
|
||||||
const e = newEmail.trim().toLowerCase();
|
const e = newEmail.trim().toLowerCase();
|
||||||
if (!e) throw new Error('邮箱不能为空。');
|
if (!e) throw new Error('Email cannot be empty.');
|
||||||
if (!username) throw new Error('用户名不能为空。');
|
if (!username) throw new Error('Username cannot be empty.');
|
||||||
const accounts = readAccounts();
|
const accounts = readAccounts();
|
||||||
const account = accounts.find((a) => a.username === username);
|
const account = accounts.find((a) => a.username === username);
|
||||||
if (!account) throw new Error(`账号不存在:${username}`);
|
if (!account) throw new Error(`Account not found: ${username}`);
|
||||||
// 检查邮箱是否已被其他人使用
|
// 检查邮箱是否已被其他人使用
|
||||||
const conflict = accounts.find((a) => a.email.toLowerCase() === e && a.username !== username);
|
const conflict = accounts.find((a) => a.email.toLowerCase() === e && a.username !== username);
|
||||||
if (conflict) throw new Error(`邮箱 "${newEmail.trim()}" 已被其他用户使用。`);
|
if (conflict) throw new Error(`Email "${newEmail.trim()}" is already in use by another user.`);
|
||||||
account.email = newEmail.trim();
|
account.email = newEmail.trim();
|
||||||
writeAccountsRaw(accounts);
|
writeAccountsRaw(accounts);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,11 +40,11 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
|
|||||||
{
|
{
|
||||||
template_id: 'builtin-pr-reviewer',
|
template_id: 'builtin-pr-reviewer',
|
||||||
name: 'PR Reviewer',
|
name: 'PR Reviewer',
|
||||||
summary: '代码合入主干前的最后一道质量关卡',
|
summary: 'The last quality gate before code merges to main',
|
||||||
builtin: true,
|
builtin: true,
|
||||||
description: '执行 pr-review workflow:核对必查项 + 给出 actionable 评论。',
|
description: 'Runs the pr-review workflow: checks required items and leaves actionable comments.',
|
||||||
role_prompt: '你是严格的 PR Reviewer,是代码合入主干前的最后一道质量关卡。',
|
role_prompt: 'You are a strict PR Reviewer, the last quality gate before code merges to main.',
|
||||||
rules_prompt: '1. 先读 PR 描述与关联 issue,明确改动意图。\n2. 必查:正确性、边界条件、安全、测试覆盖、命名与可读性。\n3. 每条评论必须 actionable,指明位置与建议改法。\n4. 阻断性问题与建议性问题分开标注。',
|
rules_prompt: '1. Read the PR description and linked issue first to understand intent.\n2. Required checks: correctness, edge cases, security, test coverage, naming and readability.\n3. Every comment must be actionable, pointing to the location and a suggested fix.\n4. Separate blocking issues from suggestions.',
|
||||||
skills: [],
|
skills: [],
|
||||||
code_graphs: [],
|
code_graphs: [],
|
||||||
llm_wikis: [],
|
llm_wikis: [],
|
||||||
@ -53,12 +53,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
template_id: 'builtin-bugfix-engineer',
|
template_id: 'builtin-bugfix-engineer',
|
||||||
name: 'Bug-fix 工程师',
|
name: 'Bug-fix Engineer',
|
||||||
summary: '复现 → 定位 → 修复 → 自测的修复 loop',
|
summary: 'Reproduce → locate → fix → self-test loop',
|
||||||
builtin: true,
|
builtin: true,
|
||||||
description: '面向 bug-fix loop 的工程师 agent:复现 → 定位 → 修复 → 自测。',
|
description: 'An engineer agent for the bug-fix loop: reproduce → locate → fix → self-test.',
|
||||||
role_prompt: '你是面向 bug-fix loop 的修复工程师,对每个缺陷负责到根因,追求最小且可验证的修复。',
|
role_prompt: 'You are a bug-fix engineer for the bug-fix loop, owning each defect down to its root cause and aiming for the smallest verifiable fix.',
|
||||||
rules_prompt: '1. 先稳定复现,再动手;无法复现先补复现信息。\n2. 定位根因而非掩盖症状。\n3. 修复保持最小 diff,附带回归测试。\n4. 自测通过后再提交,说明验证方式。',
|
rules_prompt: '1. Get a stable repro before touching anything; if it cannot be reproduced, gather repro info first.\n2. Locate the root cause rather than masking the symptom.\n3. Keep the fix a minimal diff with an accompanying regression test.\n4. Self-test before submitting, and state how it was verified.',
|
||||||
skills: [],
|
skills: [],
|
||||||
code_graphs: [],
|
code_graphs: [],
|
||||||
llm_wikis: [],
|
llm_wikis: [],
|
||||||
@ -67,12 +67,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
template_id: 'builtin-issue-triage',
|
template_id: 'builtin-issue-triage',
|
||||||
name: 'Issue 分诊员',
|
name: 'Issue Triager',
|
||||||
summary: '新进 issue 的第一接待人:分类 / 补全 / 指派',
|
summary: 'First responder for incoming issues: classify / complete / assign',
|
||||||
builtin: true,
|
builtin: true,
|
||||||
description: '面向新进 issue:判断类型、补充复现信息、指派 owner。',
|
description: 'For incoming issues: determines the type, fills in missing repro info, and assigns an owner.',
|
||||||
role_prompt: '你是 issue 分诊员,是新进 issue 的第一接待人,负责分类、补全信息并指派。',
|
role_prompt: 'You are an issue triager, the first responder for incoming issues, responsible for classifying, completing information, and assigning them.',
|
||||||
rules_prompt: '1. 判断类型(bug / feature / question / 重复)。\n2. 缺信息时按模板向报告者追问复现步骤、环境、期望。\n3. 标注优先级与影响面。\n4. 指派合适 owner 并说明理由。',
|
rules_prompt: '1. Determine the type (bug / feature / question / duplicate).\n2. When info is missing, use a template to ask the reporter for repro steps, environment, and expected behavior.\n3. Label priority and impact.\n4. Assign a suitable owner with reasoning.',
|
||||||
skills: [],
|
skills: [],
|
||||||
code_graphs: [],
|
code_graphs: [],
|
||||||
llm_wikis: [],
|
llm_wikis: [],
|
||||||
@ -81,12 +81,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
template_id: 'builtin-doc-engineer',
|
template_id: 'builtin-doc-engineer',
|
||||||
name: '文档工程师',
|
name: 'Documentation Engineer',
|
||||||
summary: '随 PR 同步更新 wiki / changelog',
|
summary: 'Keeps the wiki / changelog updated alongside each PR',
|
||||||
builtin: true,
|
builtin: true,
|
||||||
description: '随 PR 同步更新 wiki / changelog,保持团队知识库与代码一致。',
|
description: 'Updates wiki / changelog alongside each PR, keeping the team knowledge base consistent with the code.',
|
||||||
role_prompt: '你是文档工程师,确保团队知识库与代码始终保持同步、可信。',
|
role_prompt: 'You are a documentation engineer, ensuring the team knowledge base stays synced with the code and trustworthy at all times.',
|
||||||
rules_prompt: '1. 每个会影响行为的 PR 都要评估文档影响。\n2. 更新 changelog,语言面向使用者而非实现者。\n3. 失效文档及时下线或标注。\n4. 文档需可被检索,附必要链接与示例。',
|
rules_prompt: "1. Assess the documentation impact of every PR that changes behavior.\n2. Update the changelog in language aimed at users, not implementers.\n3. Retire or flag stale docs promptly.\n4. Docs must be searchable, with necessary links and examples.",
|
||||||
skills: [],
|
skills: [],
|
||||||
code_graphs: [],
|
code_graphs: [],
|
||||||
llm_wikis: [],
|
llm_wikis: [],
|
||||||
@ -128,7 +128,7 @@ export function createAgentTemplate(input: {
|
|||||||
chat_memories?: string[];
|
chat_memories?: string[];
|
||||||
}): AgentTemplate {
|
}): AgentTemplate {
|
||||||
const name = input.name.trim();
|
const name = input.name.trim();
|
||||||
if (!name) throw new Error('createAgentTemplate: 模板名不能为空。');
|
if (!name) throw new Error('createAgentTemplate: template name cannot be empty.');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tpl: AgentTemplate = {
|
const tpl: AgentTemplate = {
|
||||||
template_id: `tpl_${now}_${Math.random().toString(36).slice(2, 8)}`,
|
template_id: `tpl_${now}_${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
|||||||
@ -118,7 +118,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[backend store] fetchTeams failed:', err);
|
console.error('[backend store] fetchTeams failed:', err);
|
||||||
set({ teamsLoading: false });
|
set({ teamsLoading: false });
|
||||||
tea.notify.error('加载团队列表失败');
|
tea.notify.error('Failed to load team list');
|
||||||
} finally {
|
} finally {
|
||||||
set({ inflightTeams: null });
|
set({ inflightTeams: null });
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
|
|||||||
Object.entries(s.inflightAgents).filter(([k]) => k !== teamId)
|
Object.entries(s.inflightAgents).filter(([k]) => k !== teamId)
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
tea.notify.error('加载 Agent 列表失败');
|
tea.notify.error('Failed to load agent list');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@ -204,7 +204,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
|
|||||||
Object.entries(s.inflightTasks).filter(([k]) => k !== teamId)
|
Object.entries(s.inflightTasks).filter(([k]) => k !== teamId)
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
tea.notify.error('加载任务列表失败');
|
tea.notify.error('Failed to load task list');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -41,6 +41,19 @@ fi
|
|||||||
|
|
||||||
rm_container_if_exists "$CONTAINER"
|
rm_container_if_exists "$CONTAINER"
|
||||||
|
|
||||||
|
# GODCALL: optional git credentials for private Gitea imports.
|
||||||
|
# Set GIT_CREDENTIALS_FILE in .env to a git credential-store file
|
||||||
|
# (one line, e.g. http://user:token@gitea-host:3000). It is mounted read-only
|
||||||
|
# so tokens never appear in repo URLs, the panel DB, or docker inspect env.
|
||||||
|
GIT_CRED_ARGS=()
|
||||||
|
if [[ -n "${GIT_CREDENTIALS_FILE:-}" && -s "$GIT_CREDENTIALS_FILE" ]]; then
|
||||||
|
GITCONFIG_FILE="$SCRIPT_DIR/.godcall-gitconfig"
|
||||||
|
printf '[credential]\n\thelper = store --file /root/.git-credentials\n' > "$GITCONFIG_FILE"
|
||||||
|
GIT_CRED_ARGS+=( -v "$GIT_CREDENTIALS_FILE:/root/.git-credentials:ro" \
|
||||||
|
-v "$GITCONFIG_FILE:/root/.gitconfig:ro" )
|
||||||
|
info "git credentials mounted from $GIT_CREDENTIALS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
# 内部 knowledge 通过 upstream memory 调 LLM 走 custom 模式,直接指向 MEMORY_LLM_*
|
# 内部 knowledge 通过 upstream memory 调 LLM 走 custom 模式,直接指向 MEMORY_LLM_*
|
||||||
# LLM_MODE=custom → 不走 memory 的 LLM proxy,而是 knowledge 直连用户提供的端点
|
# LLM_MODE=custom → 不走 memory 的 LLM proxy,而是 knowledge 直连用户提供的端点
|
||||||
info "启动 memory-hub (image=$MEMORY_HUB_IMAGE, panel=$PANEL_PORT knowledge=$KNOWLEDGE_PORT)"
|
info "启动 memory-hub (image=$MEMORY_HUB_IMAGE, panel=$PANEL_PORT knowledge=$KNOWLEDGE_PORT)"
|
||||||
@ -51,6 +64,9 @@ $DOCKER run -d --name "$CONTAINER" \
|
|||||||
-p "${PANEL_PORT}:8125" \
|
-p "${PANEL_PORT}:8125" \
|
||||||
-p "${KNOWLEDGE_PORT}:8424" \
|
-p "${KNOWLEDGE_PORT}:8424" \
|
||||||
-v "${PANEL_VOLUME}:/data/knowledge" \
|
-v "${PANEL_VOLUME}:/data/knowledge" \
|
||||||
|
${GIT_CRED_ARGS[@]+"${GIT_CRED_ARGS[@]}"} \
|
||||||
|
-e KNOWLEDGE_ALLOW_HTTP="${KNOWLEDGE_ALLOW_HTTP:-}" \
|
||||||
|
-e KNOWLEDGE_SSRF_CHECK="${KNOWLEDGE_SSRF_CHECK:-}" \
|
||||||
-e PANEL_PORT=8125 \
|
-e PANEL_PORT=8125 \
|
||||||
-e KNOWLEDGE_PORT=8424 \
|
-e KNOWLEDGE_PORT=8424 \
|
||||||
-e KNOWLEDGE_PUBLIC_BASE_URL="$KNOWLEDGE_PUBLIC_BASE_URL" \
|
-e KNOWLEDGE_PUBLIC_BASE_URL="$KNOWLEDGE_PUBLIC_BASE_URL" \
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
FROM node:22-slim AS base
|
FROM node:22-slim AS base
|
||||||
|
|
||||||
RUN sed -i 's|deb.debian.org|mirrors.tencent.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
|
# GODCALL: upstream rewrote apt sources to mirrors.tencent.com here — unreachable
|
||||||
sed -i 's|deb.debian.org|mirrors.tencent.com|g' /etc/apt/sources.list 2>/dev/null || true
|
# outside Tencent's network; use stock Debian mirrors.
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 make g++ git curl ca-certificates \
|
python3 make g++ git curl ca-certificates \
|
||||||
|
|||||||
87
tools/import-gitea.mjs
Normal file
87
tools/import-gitea.mjs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* GODCALL — bulk-import every Gitea repo as a CodeGraph.
|
||||||
|
*
|
||||||
|
* Enumerates repos via the Gitea API and registers each with the knowledge
|
||||||
|
* service. Clone auth is handled by the git credential store mounted into the
|
||||||
|
* memory-hub container (see start-memory-hub.sh GIT_CREDENTIALS_FILE) — tokens
|
||||||
|
* never go into repo URLs.
|
||||||
|
*
|
||||||
|
* Env:
|
||||||
|
* GITEA_URL e.g. http://100.71.119.27:3000
|
||||||
|
* GITEA_TOKEN_FILE file containing a Gitea access token (read:repository)
|
||||||
|
* KNOWLEDGE_URL e.g. http://100.91.239.7:8424
|
||||||
|
* TEAM_ID godcall team id (team-...)
|
||||||
|
* USER_ID godcall user id (usr-...)
|
||||||
|
* GITEA_CLONE_BASE optional override when Gitea ROOT_URL differs from the
|
||||||
|
* address reachable from the hub container
|
||||||
|
* ONLY optional comma-separated repo names to import
|
||||||
|
* SKIP optional comma-separated repo names to skip
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const need = (k) => {
|
||||||
|
const v = process.env[k];
|
||||||
|
if (!v) { console.error(`missing env ${k}`); process.exit(1); }
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GITEA_URL = need("GITEA_URL").replace(/\/$/, "");
|
||||||
|
const TOKEN = readFileSync(need("GITEA_TOKEN_FILE"), "utf8").trim();
|
||||||
|
const KNOWLEDGE_URL = need("KNOWLEDGE_URL").replace(/\/$/, "");
|
||||||
|
const TEAM_ID = need("TEAM_ID");
|
||||||
|
const USER_ID = need("USER_ID");
|
||||||
|
const CLONE_BASE = (process.env.GITEA_CLONE_BASE || GITEA_URL).replace(/\/$/, "");
|
||||||
|
const ONLY = (process.env.ONLY || "").split(",").map(s => s.trim()).filter(Boolean);
|
||||||
|
const SKIP = new Set((process.env.SKIP || "").split(",").map(s => s.trim()).filter(Boolean));
|
||||||
|
|
||||||
|
async function giteaRepos() {
|
||||||
|
const repos = [];
|
||||||
|
for (let page = 1; ; page++) {
|
||||||
|
const r = await fetch(`${GITEA_URL}/api/v1/repos/search?limit=50&page=${page}`, {
|
||||||
|
headers: { Authorization: `token ${TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`gitea search ${r.status}: ${await r.text()}`);
|
||||||
|
const { data } = await r.json();
|
||||||
|
if (!data?.length) break;
|
||||||
|
repos.push(...data);
|
||||||
|
if (data.length < 50) break;
|
||||||
|
}
|
||||||
|
return repos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCodeGraph(repo) {
|
||||||
|
const repoUrl = `${CLONE_BASE}/${repo.full_name}.git`;
|
||||||
|
const body = {
|
||||||
|
team_id: TEAM_ID,
|
||||||
|
user_id: USER_ID,
|
||||||
|
repo_url: repoUrl,
|
||||||
|
branch: repo.default_branch || "main",
|
||||||
|
repo_name: repo.name,
|
||||||
|
};
|
||||||
|
const r = await fetch(`${KNOWLEDGE_URL}/v3/code-graph/create`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "x-tdai-service-id": "default" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const json = await r.json().catch(() => ({}));
|
||||||
|
return { status: r.status, id: json?.data?.code_graph_id, msg: json?.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = await giteaRepos();
|
||||||
|
console.log(`gitea reports ${repos.length} repos`);
|
||||||
|
let ok = 0, fail = 0;
|
||||||
|
for (const repo of repos) {
|
||||||
|
if (ONLY.length && !ONLY.includes(repo.name)) continue;
|
||||||
|
if (SKIP.has(repo.name)) { console.log(`skip ${repo.full_name}`); continue; }
|
||||||
|
try {
|
||||||
|
const res = await createCodeGraph(repo);
|
||||||
|
const good = res.status === 200 || res.status === 201;
|
||||||
|
good ? ok++ : fail++;
|
||||||
|
console.log(`${good ? "ok " : "FAIL"} ${repo.full_name} [${repo.default_branch}] -> ${res.id ?? res.msg ?? res.status}`);
|
||||||
|
} catch (e) {
|
||||||
|
fail++;
|
||||||
|
console.log(`FAIL ${repo.full_name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`done: ${ok} registered, ${fail} failed`);
|
||||||
Loading…
Reference in New Issue
Block a user