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:
type-two 2026-08-03 21:13:08 +10:00
parent f3df79326d
commit 260f02915f
62 changed files with 2006 additions and 1855 deletions

31
MemoryCore/Dockerfile Normal file
View 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"]

View File

@ -13,125 +13,125 @@ import type { MemoryRecord, ExtractedMemory } from "../record/l1-writer.js";
// 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 ** typepersona / episodic / instruction / work_fact / work_task / work_method / work_artifact/****
- ****/**** target_ids
- typemerged_type
- **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.
- **Many-to-many merging**: one new memory may simultaneously replace/merge **multiple** existing memories in the candidate pool (specified via the target_ids array).
- After merging, you must decide the new memory's best type (merged_type).
##
## Judgment logic
1. ****
- ****persona/instruction
- ****episodic
1. **Distinguish the memory's nature**:
- **State-like** (persona/instruction): preferences, traits, long-term settings, relatively stable facts, behavior rules
- **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. ****
- "store"
- "skip"
- "update"/
- "merge"
3. **Choose the action**:
- "store": treat as new information; add the current memory.
- "skip": the existing memory is better the new memory adds nothing or is vaguer; ignore the current memory.
- "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": 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. ****
- / merge skip update
- merge skip
- episodic "用户在 2018 年开始做播客" + persona "用户有播客制作经验" merge persona episodic
4. **Strategy tendencies**:
- State-like: multiple descriptions of the same preference/trait lean merge; no new information skip; an explicit update update
- Event-like: cause/effect or different phases of the same event lean merge into one complete narrative; exact duplicate skip
- 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 **
- merge / update merged_timestamps ****
- 线
5. **timestamp handling**:
- 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",
"target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
"merged_content": "合并/更新后的记忆内容merge/update 时必填)",
"merged_type": "合并后的最佳 typepersona|episodic|instruction|work_fact|work_task|work_method|work_artifactmerge/update 时必填)",
"target_ids": ["candidate memory record_id 1 to delete", "record_id 2"],
"merged_content": "the merged/updated memory content (required for 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_timestamps": ["合并后的时间戳数组包含所有新旧记忆时间戳的并集merge/update 时必填)"]
"merged_timestamps": ["the merged timestamp array, containing the union of all old and new memory timestamps (required for merge/update)"]
}
]
- target_ids ID **** 1 store/skip
- merged_contentmerge/update store/skip
- merged_typemerge/update type
- merged_prioritymerge/update 0-100 merge/update **** priority priority 70 8080-100/60-79/<60次要信息
- merged_timestamps + `;
Field notes:
- 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: the final memory text for merge/update. Omit for store/skip.
- merged_type: the type the memory should belong to after merge/update. Judge from the essential nature of the merged content.
- 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: 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 ** typework_fact / work_task / work_method / work_artifact****
- ****/**** target_ids
- typemerged_type
-
- **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.
- **Many-to-many merging**: one new memory may simultaneously replace/merge **multiple** existing memories in the candidate pool (specified via the target_ids array).
- 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. ****
- **work_fact**
- **work_task**ownerdeadline
- **work_method**SOPAgent
- **work_artifact**PRIssuePrompt稿
1. **Distinguish the memory's nature**:
- **Work-fact-like (work_fact)**: project facts, requirements, decisions, status, risks, constraints, experiment results, customer feedback.
- **Work-task-like (work_task)**: to-dos, owner, deadline, next-step plans, task status changes.
- **Work-method-like (work_method)**: SOPs, taboos, principles, experience, design rationale, judgment criteria, Agent behavior rules.
- **Work-artifact-like (work_artifact)**: documents, PRs, Issues, Prompts, reports, code branches, design drafts, links, etc.
2. **/**
- scene_name
-
-
2. **Decide whether it is the same work object / evolving process**:
- 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. ****
- "store"
- "skip"
- "update"
- "merge"
3. **Choose the action**:
- "store": treat as new information; add the current memory.
- "skip": the existing memory is better the new memory adds nothing or is vaguer; ignore the current memory.
- "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": 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. ****
- work_fact// update merge
- work_task ownerdeadline update merge
- work_method SOP merge update
- work_artifactPRPrompt merge update
- work_fact "团队决定 L1 type 保持少量高层分类" + work_method "L1 type 不宜过细,否则影响 L2/L3 聚合" merge work_method
4. **Strategy tendencies**:
- work_fact: additions or corrections to the same fact/decision/status lean update or merge.
- work_task: owner, deadline, or status changes of the same task lean update; added dependencies or acceptance criteria lean merge.
- work_method: additions to the same SOP, taboo, principle, or experience lean merge; a clearer, more general formulation lean update.
- work_artifact: additions of usage, version, or links for the same document, PR, Prompt, report, etc. lean merge or update.
- 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 **
- merge / update merged_timestamps ****
- 线
5. **timestamp handling**:
- 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",
"target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
"merged_content": "合并/更新后的记忆内容merge/update 时必填)",
"merged_type": "合并后的最佳 typework_fact|work_task|work_method|work_artifactmerge/update 时必填)",
"target_ids": ["candidate memory record_id 1 to delete", "record_id 2"],
"merged_content": "the merged/updated memory content (required for 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_timestamps": ["合并后的时间戳数组包含所有新旧记忆时间戳的并集merge/update 时必填)"]
"merged_timestamps": ["the merged timestamp array, containing the union of all old and new memory timestamps (required for merge/update)"]
}
]
- target_ids ID **** 1 store/skip
- merged_contentmerge/update store/skip
- merged_typemerge/update type
- merged_prioritymerge/update 0-100 merge/update **** priority80-100///60-79<60次要信息
- merged_timestamps + `;
Field notes:
- 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: the final memory text for merge/update. Omit for store/skip.
- merged_type: the type the memory should belong to after merge/update. Judge from the essential nature of the merged content.
- 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: 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 {
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;
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 {
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
@ -200,7 +200,7 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
const relatedNote =
relatedIds.length > 0
? JSON.stringify(relatedIds)
: "[](无相似候选,直接 store";
: "[] (no similar candidates — store directly)";
const memStr = JSON.stringify(
{
@ -214,7 +214,7 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
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(
@ -222,15 +222,15 @@ export function formatBatchConflictPrompt(matches: CandidateMatch[]): string {
);
// 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}
${"═".repeat(50)}
## ${matches.length}
## New Memories to Judge (${matches.length} total)
${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.`;
}

View File

@ -12,364 +12,364 @@ import type { ConversationMessage } from "../conversation/l0-recorder.js";
// System Prompt
// ============================
export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `你是专业的"情境切分与记忆提取专家"。
persona, episodic, instruction
export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `You are a professional "Scene Segmentation and Memory Extraction Expert".
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
- 沿
- "换话题"
-
- "我AI在和xxx用户身份做xxx目标活动"**使** 30-50
### 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).
- 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].
1. "这次、本单"
2. "跳出当前对话依然成立""用户(姓名)""AI"
3.
[General extraction principles]
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. 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. Consolidate and merge: multiple messages with strong correlation or a causal relationship MUST be merged into one complete memory never fragment them.
> "提取句式""触发词"** \`content\` 必须按上述输出语言书写**(例如英文用户 → "The user (Maya) is a senior product manager based in Berlin")。
[The three supported types] (type rules must be followed strictly)
> 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")
-
- "用户([姓名])喜欢/是/擅长..."
- (priority)80-100//50-70/<50模糊次要可丢弃
- ...
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 ..."
- 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")
-
- "用户([姓名])在 [最好是精确绝对时间] 于 [地点] [做了某事(可以包含起因、经过、结果)]"
- timestamp metadata activity_start_time activity_end_timeISO 8601
- (priority)80-100/60-70<60琐碎事项直接丢弃
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)]".
- 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.
- Scoring (priority): 80-100 (important events / plans); 60-70 (ordinary complete activities); <60 (trivial items discard directly).
3. (type: "instruction")
- AI
- "用户要求/希望 AI 以后回答时..."
-
- (priority)-190-10070-80<70临时要求直接丢弃
3. Global instruction memory (type: "instruction")
- Definition: long-term behavior rules, format preferences, or tone controls the user gives the AI.
- Sentence pattern: "The user requires / wants the AI, in future replies, to ..."
- Trigger words: "from now on", "always", "remember", "must".
- Scoring (priority): -1 (extremely strict global hard rule); 90-100 (core behavior rules); 70-80 (important requirements); <70 (temporary requests discard directly).
---
###
- "这次帮我翻译一下"
- "这次、本单"
- AI助手自身的行为或输出
- 3
-
### 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")
- Repeated content; the AI assistant's own behavior or output
- Information that does not belong to the 3 types above
- Purely subjective feelings (emotional expressions with no objective event attached)
---
### JSON
JSON
### Task 3: Output Format Specification (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": "当前生成或继承的情境名称",
"message_ids": ["属于该情境的消息ID列表"],
"scene_name": "the scene name generated or inherited for this segment",
"message_ids": ["list of message IDs belonging to this scene"],
"memories": [
{
"content": "完整、独立的记忆陈述(按对应类型的句式要求)",
"content": "complete, self-contained memory statement (following the sentence pattern required for its type)",
"type": "persona|episodic|instruction",
"priority": 80,
"source_message_ids": ["消息ID_1", "消息ID_2"],
"source_message_ids": ["message_id_1", "message_id_2"],
"metadata": {}
}
]
}
]
metadata
- episodic {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
- {}
metadata field notes:
- 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"],
"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 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.
1. IssuePR
2. "需求讨论""上线排期"
3. 线线
4.
[Switch conditions]
Switch to or create a new scene when any of the following occurs:
1. The subject of discussion becomes a different project, module, requirement, customer, Issue, PR, experiment, incident, or deliverable.
2. The work goal clearly changes, e.g. from "requirements discussion" to "release scheduling".
3. A new independent task, decision thread, or troubleshooting thread clearly appears.
4. Multiple work topics appear back-to-back within the same batch of messages split them into multiple scenes.
-
- "团队在围绕[项目/模块/议题]推进[目标活动]"
- 30-50
-
- "团队在围绕 Agent Memory 群聊抽取设计共享记忆规则"
- "团队在围绕 Billing API 排查线上超时问题"
- "团队在围绕安灯试点确认查询接口需求"
[Naming rules]
- The scene name must be centered on the work object.
- Recommended format: "The team is advancing [target activity] around [project/module/topic]".
- Roughly 30-50 characters or equivalent length, a single sentence, globally unique.
- Examples:
- "The team is designing shared-memory rules around Agent Memory group-chat extraction"
- "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.
- Agent
-
1. Oriented toward work collaboration:
- 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.
-
- content
- 使"这个""那个""上面说的"
3. Self-contained and complete:
- Each memory must remain understandable outside the current conversation.
- 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.
-
-
- source_message_ids message id
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 must only contain message ids from the [New Messages to Extract From].
7. AI / Agent
- AI
- Agent
- AI 使 work_artifact work_method
7. Handling AI / Agent output:
- Do not automatically treat AI suggestions as team facts or team decisions.
- 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-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
- "Agent Memory 团队版采用 L0 Work Event、L1 Work Record、L2 Project Scene Block、L3 Team Operating Memory 的四层结构。"
- "团队决定团队共享记忆只提取工作内容,不沉淀个人画像。"
- "安灯试点要求记忆查询接口支持按项目筛选,并允许配置返回字段。"
- "多人群聊中工作讨论和闲聊混杂,存在误提取无关内容的风险。"
Examples:
- "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
- 90-100
- 70-89
- <70细碎临时低影响事实直接丢弃
priority:
- 90-100: key decisions, core requirements, long-term constraints, important risks.
- 70-89: ordinary facts with lasting value for the current project.
- <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.
-
- owner
- deadline
-
-
-
-
Suitable to extract:
- To-do items
- Tasks with a clear owner
- Tasks with a clear deadline
- Issues needing follow-up
- Blocked items
- Next-step plans
- Task status changes
- "后端团队需要在周五前完成 record 与 event 多对多追溯表结构设计。"
- "产品侧需要补充团队共享记忆的权限边界说明。"
- "L1 Prompt 已进入工作记忆类型收敛阶段,下一步需要同步修改下游 enum。"
Examples:
- "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."
- "The L1 Prompt has entered the work-memory type convergence phase; the next step is to update the downstream enum accordingly."
priority
- 90-100 deadline
- 70-89 owner
- <70模糊临时无明确后续动作的待办直接丢弃
priority:
- 90-100: tasks blocking delivery, with a hard deadline, or on the critical path.
- 70-89: ordinary tasks with a clear owner or clear follow-up action.
- <70: vague, temporary to-dos with no clear follow-up action discard directly.
metadata
- owner {"owner": "名称或ID"}
- deadline {"deadline": "ISO8601"}
- {"status": "todo|doing|done|blocked|deferred|cancelled"}
metadata suggestions:
- If the owner can be determined, fill in {"owner": "name or ID"}.
- If the deadline can be determined, fill in {"deadline": "ISO8601"}.
- 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")
SOPAgent
Definition:
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.
- SOP
-
-
- 线
-
-
-
-
- Agent
- Prompt
-
Suitable to extract:
- SOPs
- Collaboration processes
- Design principles
- Technical-direction reasoning
- Evaluation criteria
- Risk-avoidance rules
- Taboos and boundaries
- Reusable experience
- Agent execution strategies
- Prompt-writing principles
- Project methodology
- "团队版 Agent Memory 的 L1 抽取应优先使用少量高层工作类型,避免把类型拆得过细导致后续聚合困难。"
- "团队共享记忆的抽取应优先记录项目事实、任务、方法和交付物,而不是普通聊天内容。"
- "当多人消息中只有单人建议而没有明确确认时,不能直接抽取为团队决策。"
- "L1 Prompt 应保持输出 JSON 结构稳定,优先通过调整 type 枚举和提取规则适配新场景。"
- "工作方法类记忆可以沉淀 SOP、禁忌、设计思路和可复用经验用于支持后续 Agent 执行。"
Examples:
- "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."
- "The L1 Prompt should keep its output JSON structure stable, adapting to new scenarios primarily by adjusting the type enum and extraction rules."
- "Work-method memories can accumulate SOPs, taboos, design rationale, and reusable experience to support future Agent execution."
priority
- 90-100 Agent
- 70-89
- <70过于临时模糊或只适用于一次性操作的方法直接丢弃
priority:
- 90-100: core methods that are long-term stable, reusable across tasks, and shape Agent behavior or team processes.
- 70-89: methods with clear reuse value for the current project's future work.
- <70: methods that are too temporary, vague, or applicable only to a one-off operation discard directly.
metadata
- {"scope": "project|team|module|agent|workflow"}
- {"method_type": "sop|principle|constraint|anti_pattern|heuristic|evaluation_criterion"}
- {"method_type": "anti_pattern"}
metadata suggestions:
- If the applicable scope can be determined, fill in {"scope": "project|team|module|agent|workflow"}.
- If the method category can be determined, fill in {"method_type": "sop|principle|constraint|anti_pattern|heuristic|evaluation_criterion"}.
- If it is a taboo or anti-pattern, fill in {"method_type": "anti_pattern"}.
---
4. type: "work_artifact"
4. Work artifact (type: "work_artifact")
使PRIssue稿Prompt
Definition:
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.
-
- PR / Issue
-
-
- 稿
-
- Prompt
-
-
-
- Agent
Suitable to extract:
- Documents
- PRs / Issues
- Code branches
- Experiment reports
- Design drafts
- Meeting notes
- Prompts
- Spreadsheets
- Links
- Proposal drafts
- Agent-generated output that was adopted
- "L1 工作记忆抽取 Prompt 是 Agent Memory 团队版设计中的核心 Prompt 资产。"
- "团队将四层工作记忆结构作为后续 L2 和 L3 聚合 Prompt 的设计基础。"
- "Flowchart 与 StateDiagram 对比实验结果可作为短期记忆压缩方案选择的依据。"
Examples:
- "The L1 work-memory extraction Prompt is a core Prompt asset in the team-edition Agent Memory design."
- "The team will use the four-layer work memory structure as the design foundation for the subsequent L2 and L3 aggregation Prompts."
- "The Flowchart vs. StateDiagram comparison experiment results can serve as the basis for choosing the short-term memory compression approach."
priority
- 90-100 PR线
- 70-89
- <70临时文件低价值链接未被采用的草稿直接丢弃
priority:
- 90-100: core documents, key PRs, release-related assets, important experiment reports.
- 70-89: ordinary work assets likely to be reused later.
- <70: temporary files, low-value links, unadopted drafts discard directly.
metadata
- {"artifact_type": "doc|pr|issue|repo|branch|design|report|prompt|dataset|meeting_note"}
- {"artifact_ref": "链接、ID或名称"}
metadata suggestions:
- If the asset type can be determined, fill in {"artifact_type": "doc|pr|issue|repo|branch|design|report|prompt|dataset|meeting_note"}.
- If a link or identifier can be determined, fill in {"artifact_ref": "link, ID, or name"}.
---
###
### What NOT to extract
-
- "这次帮我改一下格式"
- AI 稿
-
-
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".
- 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": "当前生成或继承的工作情境名称",
"message_ids": ["属于该情境的消息ID列表"],
"scene_name": "the work scene name generated or inherited for this segment",
"message_ids": ["list of message IDs belonging to this scene"],
"memories": [
{
"content": "完整、独立、适合团队共享的工作记忆陈述",
"content": "complete, self-contained work memory statement suitable for team sharing",
"type": "work_fact|work_task|work_method|work_artifact",
"priority": 80,
"source_message_ids": ["消息ID_1", "消息ID_2"],
"source_message_ids": ["message_id_1", "message_id_2"],
"metadata": {}
}
]
}
]
metadata
- {}
- work_task ownerdeadlinestatus
- work_method scopemethod_type
- work_artifact artifact_typeartifact_ref
- work_fact work_objectstatusactivity_start_timeactivity_end_time
- metadata
metadata field notes:
- All types may output an empty object {}.
- work_task may add owner, deadline, status.
- work_method may add scope, method_type.
- work_artifact may add artifact_type, artifact_ref.
- work_fact may add work_object, status, activity_start_time, activity_end_time.
- 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"],
"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 {
return mode === "code" ? EXTRACT_WORK_MEMORIES_SYSTEM_PROMPT : EXTRACT_MEMORIES_SYSTEM_PROMPT;
@ -391,27 +391,27 @@ export function formatExtractionPrompt(params: {
backgroundMessages?: ConversationMessage[];
previousSceneName?: string;
}): string {
const { newMessages, backgroundMessages = [], previousSceneName = "" } = params;
const { newMessages, backgroundMessages = [], previousSceneName = "None" } = params;
const bgText = backgroundMessages.length > 0
? backgroundMessages
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
.join("\n\n")
: "";
: "None";
const newText = newMessages
.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`)
.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}
timestamp
[New Messages to Extract From] (be sure to infer absolute times from the timestamps; extract memories ONLY from here!):
${newText}`;
}

View File

@ -36,232 +36,232 @@ export interface PersonaPromptResult {
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\`
- ** / **使 **write** \`path\`=\`persona.md\`, \`content\`=完整内容
- ****使 **edit** \`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: 旧内容片段, \`newText\`: 新内容片段}]
2. ** \`persona.md\` 这一个文件**,禁止读取或写入任何其他文件(包括 scene_blocks/、.metadata/ 等)。
3. ** persona ** persona
4. ** read ** 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\`.
- **First generation / major rewrite**: use the **write** tool to write the whole file. Parameters: \`path\`=\`persona.md\`, \`content\`=full content
- **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. **Only \`persona.md\` may be operated on** — reading or writing any other file (including scene_blocks/, .metadata/, etc.) is forbidden.
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. **No read tool needed**: the full current content of persona.md is already provided in the user message update directly from it.
### 🚫
- ****persona.md 2000
- ****
- **使**Persona workspace
- ** persona.md **
### 🚫 Strictly forbidden
- **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!
- **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.
- **No operations on any file other than persona.md**.
---
## (The Core Logic)
## The Core Logic
🧠 (Connect & Synthesize)
"叙事连贯性" No Bullet-point Spamming
🧠 Core thinking engine: Connect & Synthesize
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) ->
* ****:
* ****: Agent ********
### 🟢 Layer 1: The Base & Facts -> [Build connection]
* **Scan target**: solid facts, demographic attributes, current state.
* **Practical value**: gives the Agent **icebreaker topics** and **context awareness**.
### 🔵 Layer 2: 兴趣图谱 (The Interest Graph) ->
* ****:
* ****: **** / /
* ****: Agent ** (Chit-chat)** ****
### 🔵 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).
* **Practical value**: enables the Agent to hold **high-quality chit-chat** and make **lifestyle recommendations**.
### 🟡 Layer 3: 交互协议 (The Interface) ->
* ****:
* ****: Agent ****
### 🟡 Layer 3: The Interface -> [Remove friction]
* **Scan target**: the user's communication habits, landmines, workflow preferences.
* **Practical value**: guides the Agent on **how to speak and how to deliver results**, avoiding landmines.
### 🔴 Layer 4: 认知内核 (The Core) ->
* ****:
* ****: Agent ****"副驾驶"
### 🔴 Layer 4: The Core -> [Deep resonance]
* **Scan target**: decision logic, contradictions, ultimate drives.
* **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
# 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 ()
*( Main Agent "为什么")*
## 🤖 Chapter 3: Interaction & Cognitive Protocol
*(This is the Main Agent's action guide. For practicality, keep this semi-structured, but explain the "why")*
### 3.1 (How to Speak)
### 3.2 (How to Think)
### 3.1 Communication Strategy (How to Speak)
### 3.2 Decision Logic (How to Think)
## 🧩 Chapter 4: Deep Insights & Evolution ()
*()*
## 🧩 Chapter 4: Deep Insights & Evolution
*(Anthropological field notes)*
* ****: []
* ****: []
* ****: 3-7 10-15
- \`TagName\` - 简短注释说明
* **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].
* **Emergent traits**: distill the 3-7 most core trait tags, each on its own line with a short annotation (10-15 characters)
- \`TagName\` - short annotation
\`\`\`\`
---
###
- **使 write edit \`persona.md\`**
-
- Chapter 4
-
-
- persona.md`;
### Success Criteria
- **The final result must be written to \`persona.md\` with the write or edit tool**
- Deep insights generated from scene evidence
- 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)
- Operate only on persona.md; do not touch other files`;
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\`**。
- / 使 **write**\`path\`=\`persona.md\`, \`content\`=完整内容。
- 使 **edit**\`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: 旧内容片段, \`newText\`: 新内容片段}]。
2. ** \`persona.md\` 这一个文件**,禁止读取或写入任何其他文件。
3. ** read ** \`persona.md\` 的完整内容已在用户消息中提供。
4. Markdown
1. **You must use the file tools to write the final content into \`persona.md\`**.
- First generation / major rewrite: use **write**, parameters: \`path\`=\`persona.md\`, \`content\`=full content.
- Incremental update: use **edit**, parameters: \`path\`=\`persona.md\`, \`edits\`=[{\`oldText\`: old content fragment, \`newText\`: new content fragment}].
2. **Only \`persona.md\` may be operated on** — reading or writing any other file is forbidden.
3. **No read tool needed**: the full current content of \`persona.md\` is already provided in the user message.
4. The written content must contain only the final Markdown document no analysis process or explanations.
## 🚫
## 🚫 Strictly forbidden
- ** 1200 ** \`persona.md\` 必须高度压缩,求精不求多。
- ****"项目 v2 要优化""某模块继续推进"
- ****
- ****PRIssue L3
- ****
- ****
- ****
- **No exceeding 1200 characters**: the final \`persona.md\` must be highly compressed — precision over volume.
- **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.
- **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**
2. **Principle**
3. **Decision Logic**
4. **Boundary**
5. **Anti-pattern**
6. **Agent Rule**Agent
1. **SOP**: what process similar future tasks should follow.
2. **Principle**: working principles the team upholds long-term.
3. **Decision Logic**: by what criteria to judge when facing trade-offs.
4. **Boundary**: what must not be done, and what must not be automated.
5. **Anti-pattern**: which practices cause errors, pollute memory, or degrade quality.
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. ****
2. ****
3. ****Agent
4. ****
5. ****
1. **Generality**: does it apply to multiple projects, multiple tasks, or multiple work contexts?
2. **Completeness**: detached from the original project, can a reader still understand what it demands?
3. **Actionability**: can the Agent change its future behavior based on it?
4. **Stability**: is it likely to stay valid long-term, rather than being one-off task state?
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:
- ****
- **** SOP Agent
- ****
- ****
- **** L3
- **Reinforce**: the new scene merely corroborates an existing principle compress into the existing sentence or leave unchanged.
- **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.
- **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
> **Operating Thesis**: [ Agent ]
> **Operating Thesis**: [One sentence capturing the team's most core, most general working method or Agent execution principle.]
## Core Principles
[]
[Only high-level principles that hold stably across work contexts. Each must be semantically complete.]
- []&#58; [ / / ]
- [Principle]&#58; [applicability condition / judgment logic / why it matters]
## Reusable SOPs
[]
[Only processes that can be executed repeatedly. Do not write project-specific steps.]
- [SOP ]&#58; [] [1] [2] [/]
- [SOP name]&#58; When [trigger condition], first [step 1], then [step 2], finally [output / acceptance criteria].
## Decision Logic
[]
[Record trade-off criteria and priorities.]
- [] [A] [B] []
- When [scenario], prefer [A] over [B], because [reason].
## Boundaries & Anti-patterns
[]
[Record taboos, boundaries, and error patterns.]
- [] [] []
- Do not [wrong practice]; instead [recommended practice], because [reason].
## 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\`
- 1200
- SOP Agent
-
-
-
- Scene Navigation
- \`persona.md\``;
- Must write to \`persona.md\` with write or edit
- Final content does not exceed 1200 characters
- 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
- Do not add scene navigation (engineering appends Scene Navigation and the scene index automatically)
- Operate only on \`persona.md\``;
// ============================
// User Prompt builder (dynamic data)
@ -282,39 +282,39 @@ export function buildPersonaPrompt(params: PersonaPromptParams): PersonaPromptRe
const isCodeMode = promptMode === "code";
const targetFile = "persona.md";
const modeLabel = mode === "first" ? "🆕 首次生成" : "🔄 迭代更新";
const modeLabel = mode === "first" ? "🆕 First generation" : "🔄 Incremental update";
const triggerSection = triggerInfo
? `\n### 触发信息\n${triggerInfo}\n`
? `\n### Trigger Info\n${triggerInfo}\n`
: "";
const existingPersonaSection = existingPersona
? isCodeMode
? `\n## 📄 当前 Team Operating Doctrine工程已预加载\n\n` +
`*以下是现有 persona.md 中 Team Operating Doctrine 的完整内容(${existingPersona.length} 字符)。更新后必须压缩在 1200 字以内:*\n\n` +
? `\n## 📄 Current Team Operating Doctrine (preloaded by engineering)\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`
: `\n## 📄 当前 Persona工程已预加载\n\n` +
`*以下是现有 persona.md 的完整内容(${existingPersona.length} 字符基于此更新后请控制在2000字内*\n\n` +
: `\n## 📄 Current Persona (preloaded by engineering)\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`
: "";
const iterationGuide = mode === "incremental"
? isCodeMode
? `\n## 🔄 迭代决策指南\n\n` +
`面对变化场景,自主判断处理方式:强化(佐证已有原则)/ 补充(新的通用 SOP、禁忌、判断逻辑或 Agent 规则)/ 修正(旧原则被更新)/ 重构(内容变长、变散、变项目化)/ 不改(只有项目状态或低层事实)。\n`
: `\n## 🔄 迭代决策指南\n\n` +
`面对变化场景,自主判断处理方式:强化(佐证已有洞察)/ 补充(新维度)/ 修正(矛盾)/ 重构(结构调整)/ 不改(无有用新增内容)。\n`
? `\n## 🔄 Iteration Decision Guide\n\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## 🔄 Iteration Decision Guide\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}
****: ${modeLabel}
** Update time**: ${currentTime}
**Mode**: ${modeLabel}
${triggerSection}
## 📊
- ****: ${totalProcessed}
- ****: ${sceneCount}
- ****: ${changedSceneCount}
## 📊 Stats
- **Total memories**: ${totalProcessed}
- **Total scenes**: ${sceneCount}
- **Changed scenes**: ${changedSceneCount} (since the last update)
---
${changedScenesContent}

View File

@ -49,148 +49,148 @@ export interface SceneExtractionPromptResult {
function buildSceneSystemPrompt(maxScenes: number): string {
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
- ****API 20
- ****
- **Source**: recalled via API in batches (20 per batch)
- **State**: fragmented, unordered
### Layer 2 (Processing): Scene Diaries
- ********
- **** L1
- ****CreateIntegrateRewrite
- ****
### Layer 2 (Processing): Scene Diaries
- **Form**: **not checklists coherent narrative documents**
- **Logic**: fuse the L1 fragments into specific scene files
- **Actions**: Create, Integrate, Rewrite
- **Forbidden**: simple list appending
L1到L2的生成任务
You are mainly responsible for the L1-to-L2 generation task.
## (Input Context)
1. (New Memory):
2. Block (Existing Blocks Map): Markdown
3. (Current Time):
## Input Context
You will receive three inputs:
1. New Memory: a batch of raw, unstructured recent memory information.
2. Existing Blocks Map: a list of the file names and summaries of all current memory blocks (Markdown files).
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.**
##
1. **使** \`技术研究-Rust学习.md\`),当前工作目录已设为场景文件目录
2. **read "已有场景文件清单"**
3. ****使 **write** \`path\`=文件名, \`content\`=完整内容
4. ****使 **edit** \`path\`=文件名, \`edits\`=[{\`oldText\`: 旧内容, \`newText\`: 新内容}]。对于大范围重写或结构性变更,建议使用 **read** + **write** 整体重写。
5. **** \`.md\` 场景文件
6. ****使 **write** \`[DELETED]\` 标记(\`path\`=文件名, \`content\`=\`[DELETED]\`)。系统会自动清理带有此标记的文件。**禁止**写入空字符串(会被系统拒绝)。**禁止**用 \`[ARCHIVE]\`\`[CONSOLIDATED]\` 等其他标记替代删除——只有 \`[DELETED]\` 标记会触发系统清理。
7. **//**"技术架构与工程实践.md""日常生活与工作节奏.md" BATCHREPORTCONSOLIDATIONINTEGRATIONARCHIVESUMMARY
## File Operation Constraints (must be strictly followed)
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 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. **When creating a new scene file**, use the **write** tool. Parameters: \`path\`=file name, \`content\`=full content
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. **The scene index and system configuration are maintained automatically by the engineering system** focus only on operating on the \`.md\` scene files
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. **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 线 \`-\`、下划线 \`_\`、点号 \`.\`
- ** \`.md\` 结尾**(小写)
- ** ** \`( ) [ ] { }\`、斜杠 \`/ \\\`、冒号 \`:\`、分号 \`;\`、问号 \`?\`、感叹号 \`!\`、星号 \`*\`、竖线 \`|\`、其他标点
- ****使 \`-\`(短横线)连接,不要用空格
- ****沿
- **Allowed characters**: English letters, digits, CJK characters, hyphen \`-\`, underscore \`_\`, dot \`.\`
- **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\`
- \`日常生活-健康管理.md\`
- \`技术研究-Rust学习.md\`
- \`Daily-Life-Health-Management.md\`
- \`Tech-Research-Rust-Learning.md\`
- \`Coffee-Yirgacheffe.md\`
- \`Daily Rhythm in Shanghai.md\`(含空格)
- \`Coffee (Yirgacheffe).md\`(含括号)
- \`Q1 Milestone?.md\`(含空格和问号)
Wrong examples (each triggers an engineering fallback rename):
- \`Daily Rhythm in Shanghai.md\` (contains spaces)
- \`Coffee (Yirgacheffe).md\` (contains parentheses)
- \`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"
2. **** ** ${maxScenes}**
3. ****
- ${maxScenes}** MERGE ** 2-4 1 **** < ${maxScenes}
- = ${maxScenes - 1}** UPDATE CREATE **
- ${maxScenes}** UPDATE MERGE **
1. **Count the current scene total**: check the current scene total noted at the top of the "Existing Scene Blocks Summary"
2. **End goal**: after processing, the number of scene files in the directory must be **strictly less than ${maxScenes}**
3. **Obey the tiered alerts**:
- 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
- Orange alert (= ${maxScenes - 1}): **only UPDATE existing scenes; CREATE of new scenes is forbidden**
- Yellow alert (approaching ${maxScenes}): **prefer UPDATE, or proactively MERGE similar scenes**
****
1. ****"Python后端开发""Go后端开发" "后端开发技术栈"
2. **线**"求职材料-JD匹配""职业发展-能力对齐" "职业发展与求职"
3. **** heat 2-3
**Merge priority** (when a merge is needed, choose in this order):
1. **Heavily overlapping topics**: e.g. "Python Backend Development" and "Go Backend Development" merge into "Backend Development Stack"
2. **Same narrative arc**: e.g. "Job-Application-JD-Matching" and "Career-Development-Skill-Alignment" merge into "Career Development and Job Search"
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
Block
使 **read**
**"已有场景文件清单"**
### Phase 2: Retrieval & Strategy Selection
Compare the new memories against the Existing Blocks Map.
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.
1. **UPDATE**: Block **read** Block **write** **edit**
2. **MERGE**:
- block
- **** Block ** ${maxScenes}**
- ****使 Block 线
- ** ** **write** \`[DELETED]\` 标记。**仅仅打标记(如 [ARCHIVE]、[CONSOLIDATED])不算删除,文件仍会占用配额。**
3. **CREATE**:
- **** < ${maxScenes}
- **CREATE ** **read** 2 CREATE CREATE
- Block
- ** 1 **
Strategy selection (in priority order):
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**:
- The merged new block should be a more general scene that encompasses the multiple existing similar scenes
- **Forced merge**: when the current Block total is ** ${maxScenes}**, you must first merge several similar scenes
- **Proactive merge**: even below the limit, if two Blocks belong to the same narrative arc, merge them to add depth
- ** 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** [last resort]:
- **Precondition**: current scene total < ${maxScenes}
- **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
- If the topic is brand new and clearly distinct from existing content, you may create a new Block
- **At most 1 new scene per batch**
** A blockUPDATE - **
****
1. **read**(\`path\`='Python后端开发.md') → 获取已有内容 A
2. + A B\`heat = 旧heat + 1\`
3. **write**(\`path\`='Python后端开发.md', \`content\`=B) → **整体重写该场景文件**
**edit**(\`path\`='Python后端开发.md', \`edits\`=[{\`oldText\`: 旧章节, \`newText\`: 新章节}]) → **局部更新某部分**
**Example A: integrating a new memory into an existing block (UPDATE in-place update)**
**Concrete steps (tool calls)**:
1. **read**(\`path\`='Python-Backend-Development.md') → get existing content A
2. Analyze the new memory + existing content A integrate into new content B (\`heat = old heat + 1\`)
3. **write**(\`path\`='Python-Backend-Development.md', \`content\`=B) → **rewrite the whole scene file**
or **edit**(\`path\`='Python-Backend-Development.md', \`edits\`=[{\`oldText\`: old section, \`newText\`: new section}]) → **partially update one part**
** B blockMERGE **
****
1. **read**(\`path\`='Python后端开发.md') → 获取内容 A
2. **read**(\`path\`='Go后端开发.md') → 获取内容 B
3. A + B + C\`heat = heatA + heatB + 1\`
4. **write**(\`path\`='后端开发技术栈.md', \`content\`=C) → 创建合并后的新文件
5. **write**(\`path\`='Python后端开发.md', \`content\`='[DELETED]') → **⚠️ 删除旧文件 A**
6. **write**(\`path\`='Go后端开发.md', \`content\`='[DELETED]') → **⚠️ 删除旧文件 B**
**** 5-6 = =
**Example B: merging multiple blocks (MERGE old files must be deleted after the merge)**
**Concrete steps (tool calls)**:
1. **read**(\`path\`='Python-Backend-Development.md') → get content A
2. **read**(\`path\`='Go-Backend-Development.md') → get content B
3. Integrate A + B + the new memory produce new content C (\`heat = heatA + heatB + 1\`)
4. **write**(\`path\`='Backend-Development-Stack.md', \`content\`=C) → create the merged new file
5. **write**(\`path\`='Python-Backend-Development.md', \`content\`='[DELETED]') → **⚠️ delete old file A**
6. **write**(\`path\`='Go-Backend-Development.md', \`content\`='[DELETED]') → **⚠️ delete old file B**
**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):
Block: heat: 1
Block: heat: heat + 1
Block: heat: sum(block的heat) + 1
### Heat Management:
New Block: heat: 1
Updated Block: heat: old 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
-----META-START-----
@ -200,263 +200,263 @@ summary: [30-40 words concise summary for indexing]
heat: [Integer]
-----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:
- ...
##
[**100**]
[示例: 用户在后端开发方面表现出对 Python 2026-02 Rust ]
## User Core Traits
[NOT a list! A coherent paragraph. The most essential user traits you have carefully inferred fewer but better, **keep within 100 characters**]
[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]
##
[**400**]
*( Trigger -> Action -> Result)*
## Core Narrative
[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]
*(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.]
##
> [] //
- [2026-01-10]: "反对加班" "接受弹性工作"ID: #987
## 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]: 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]
reason: 具体原因描述
reason: concrete description of the reason
[/PERSONA_UPDATE_REQUEST]
****使
- 使 **read**
- 使 **write** ****
- 使 **edit** ****
- ****使 **write**(\`path\`=文件名, \`content\`='[DELETED]') 写入删除标记。系统会自动清理这些文件。**重要**:只有 \`[DELETED]\` 标记会触发系统清理。写入空字符串会被系统拒绝,写入 \`[ARCHIVE]\`\`[CONSOLIDATED]\` 等标记**不会删除文件**,文件会继续占用场景配额。`;
**Performing file operations** (tools must be used):
- Use **read** to read the scene file that needs updating
- Use **write** to create a new file or **fully rewrite** an existing scene file
- Use **edit** for **partial updates** to a scene file (e.g. updating just one section)
- **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 {
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.
- SOP
-
-
-
- Agent
From project facts, task progress, decision discussions, and delivered assets, you must distill:
- 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
- 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
- ****L1
- ****work_fact / work_task / work_method / work_artifact
- ****
- **Source**: structured work memories produced by L1 extraction
- **Types**: work_fact / work_task / work_method / work_artifact
- **State**: fragmented, partial, delivered in batches
### Layer 2 (Processing): Reusable Work Method Scene Blocks
- ****Markdown
- **** L1 SOP
- ****CreateUpdateMergeRewrite
- ****
- **Form**: Markdown work-method scene documents
- **Logic**: distill reusable SOPs, judgment logic, taboos, principles, and experience from L1 work memories, organized by method system
- **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
2. Scene Blocks Summary L2
3. (Current Time)
1. New Memories List: a batch of L1 work memories.
2. Existing Scene Blocks Summary: the file names and summaries of all current L2 scene files.
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\`),当前工作目录已设为场景文件目录。
2. **read "已有场景文件清单"**
3. ****使 **write** \`path\`=文件名, \`content\`=完整内容。
4. ****使 **edit** \`path\`=文件名, \`edits\`=[{\`oldText\`: 旧内容, \`newText\`: 新内容}]。对于大范围重写或结构性变更,建议使用 **read** + **write** 整体重写。
5. **** \`.md\` 场景文件。
6. ****使 **write** \`[DELETED]\` 标记(\`path\`=文件名, \`content\`=\`[DELETED]\`)。系统会自动清理带有此标记的文件。**禁止**写入空字符串。**禁止**用 \`[ARCHIVE]\`\`[CONSOLIDATED]\` 等其他标记替代删除。
7. **//** \`Agent-Memory-群聊抽取.md\`\`后端接口-查询能力.md\`\`团队记忆-SOP与禁忌.md\`。禁止创建以 BATCH、REPORT、CONSOLIDATION、INTEGRATION、ARCHIVE、SUMMARY 等为前缀的文件。
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 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. **When creating a new scene file**, use the **write** tool. Parameters: \`path\`=file name, \`content\`=full content.
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. **The scene index and system configuration are maintained automatically by the engineering system** focus only on operating on the \`.md\` scene files.
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. **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 线 \`-\`、下划线 \`_\`、点号 \`.\`
- ** \`.md\` 结尾**(小写)
- ** ** \`( ) [ ] { }\`、斜杠 \`/ \\\`、冒号 \`:\`、分号 \`;\`、问号 \`?\`、感叹号 \`!\`、星号 \`*\`、竖线 \`|\`、其他标点
- ****使 \`-\` 连接,不要用空格
- ****沿
- **Allowed characters**: English letters, digits, CJK characters, hyphen \`-\`, underscore \`_\`, dot \`.\`
- **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
- \`Agent-Memory-群聊抽取.md\`
- \`后端接口-查询能力.md\`
- \`团队记忆-SOP与禁忌.md\`
Correct examples:
- \`Agent-Memory-Group-Chat-Extraction.md\`
- \`Backend-API-Query-Capability.md\`
- \`Team-Memory-SOP-and-Taboos.md\`
- \`OpenClaw-Memory-Plugin.md\`
- \`Agent Memory 群聊抽取.md\`
- \`团队记忆(SOP).md\`
Wrong examples:
- \`Agent Memory Group Chat Extraction.md\`
- \`Team-Memory(SOP).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"
2. **** ** ${maxScenes}**
3. ****
- ${maxScenes}** MERGE ** 2-4 1 **** < ${maxScenes}
- = ${maxScenes - 1}** UPDATE CREATE **
- ${maxScenes}** UPDATE MERGE **
1. **Count the current scene total**: check the current scene total noted at the top of the "Existing Scene Blocks Summary".
2. **End goal**: after processing, the number of scene files in the directory must be **strictly less than ${maxScenes}**.
3. **Obey the tiered alerts**:
- 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.
- Orange alert (= ${maxScenes - 1}): **only UPDATE existing scenes; CREATE of new scenes is forbidden**.
- Yellow alert (approaching ${maxScenes}): **prefer UPDATE, or proactively MERGE similar scenes**.
****
1. ****"群聊记忆抽取""团队共享记忆抽取" "团队共享记忆-抽取策略"
2. ****"L1 Prompt 设计""L1 冲突检测" "团队版-Agent-Memory-L1管线"
3. ****"Prompt 编写原则""记忆抽取禁忌" "团队记忆-SOP与禁忌"
4. **** heat 2-3
**Merge priority**:
1. **Heavily overlapping work objects**: e.g. "Group-Chat Memory Extraction" and "Team-Shared Memory Extraction" merge into "Team-Shared-Memory-Extraction-Strategy"
2. **Same project thread**: e.g. "L1 Prompt Design" and "L1 Conflict Detection" merge into "Team-Edition-Agent-Memory-L1-Pipeline"
3. **Same method system**: e.g. "Prompt Writing Principles" and "Memory Extraction Taboos" merge into "Team-Memory-SOP-and-Taboos"
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
使 **read**
Compare the new memories against the Existing Scene Blocks Summary.
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, 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.
- You may use **write** for a full rewrite, or **edit** for partial replacement.
2. **MERGE**
- block
- **** Block ** ${maxScenes}**
- ****使 Block
- ** ** **write** \`[DELETED]\` 标记。
2. **MERGE**
- The merged new block should be a more general work scene that encompasses several similar scenes.
- **Forced merge**: when the current Block total is ** ${maxScenes}**, you must first merge several similar scenes.
- **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.
- ** 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**
- **** < ${maxScenes}
- **CREATE ** **read** 2 CREATE
- Block
- ** 1 **
3. **CREATE [last resort]**
- **Precondition**: current scene total < ${maxScenes}
- **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.
- If the topic is brand new and clearly distinct from existing content, you may create a new Block.
- **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
- **SOP**
- ****
- ****
- ****
- **** Agent
Methodology distillation: the core output of each Scene Block is reusable work method. Focus on:
- **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
- **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.
2. SOP
3. SOP
4.
5. ownerreviewerdecision maker
6. md 1500
1. A scene file is not a project daily report, chat digest, or task list. Its core content is distilled method.
2. Core sections should be mostly coherent paragraphs; short lists are allowed when needed to express SOP steps, taboos, or open items.
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. Do not write personal profiles; do not infer personal character, preferences, or private states.
5. Work roles, owner, reviewer, and decision maker may be recorded, but only in service of explaining a method's applicability conditions.
6. Keep each md within 1500 characters, prioritizing reusable, executable methodology.
---
### (Heat Management)
### Heat Management
- Block: heat: 1
- Block: heat: heat + 1
- Block: heat: sum( block heat) + 1
- New Block: heat: 1
- Updated Block: heat: old 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
-----META-START-----
@ -466,63 +466,63 @@ summary: [30-40 words concise summary for indexing, focusing on reusable method
heat: [Integer]
-----META-END-----
##
[ Scene Block ]
## Work Scenario
[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.]
##
[Agent ]
## Applicability Conditions
[Explain when this method applies: project phase, task type, risk context, team constraints, Agent execution scenarios, etc.]
## SOP
[ Agent ]
## Core SOP
[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.]
- [/]&#58; []
- [Step/rule]&#58; [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.]
- []&#58; [ / / ]
- [What not to do]&#58; [reason / consequence / alternative]
##
[ SOP ]
## Key Supporting Facts
[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.]
##
[ownerdeadlinePromptPRIssue]
## Related Tasks & Artifacts
[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]&#58; "..." "..."...
- [2026-01-10]&#58; Adjusted from "..." to "..."; reason: ...
##
[ SOP]
## Open Questions
[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)
****
- SOP
-
- Scene Block
- Agent L3 Team Operating Memory
**Trigger conditions**:
- 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.
- A key decision affects multiple Scene Blocks.
- 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]
reason: 具体原因描述
reason: concrete description of the reason
[/PERSONA_UPDATE_REQUEST]
---
**使**
- 使 **read**
- 使 **write**
- 使 **edit**
- ****使 **write**(\`path\`=文件名, \`content\`='[DELETED]') 写入删除标记。系统会自动清理这些文件。**重要**:只有 \`[DELETED]\` 标记会触发系统清理。写入空字符串会被系统拒绝,写入 \`[ARCHIVE]\`\`[CONSOLIDATED]\` 等标记不会删除文件。`;
**Performing file operations (tools must be used)**:
- Use **read** to read the scene file that needs updating.
- Use **write** to create a new file or fully rewrite an existing scene file.
- Use **edit** for partial updates to a scene file.
- **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 {
@ -545,14 +545,14 @@ export function buildSceneExtractionPrompt(params: SceneExtractionPromptParams):
} = params;
const warningSection = sceneCountWarning
? `\n⚠ **场景数量警告**: ${sceneCountWarning}\n`
? `\n⚠ **Scene count warning**: ${sceneCountWarning}\n`
: "";
const fileListSection = existingSceneFiles && existingSceneFiles.length > 0
? `### 📁 已有场景文件清单(仅以下文件可 read\n${existingSceneFiles.map((f) => `- \`${f}\``).join("\n")}\n`
: `### 📁 已有场景文件清单\n当前无已有场景文件\n`;
? `### 📁 Existing Scene Files List (only the files below may be read)\n${existingSceneFiles.map((f) => `- \`${f}\``).join("\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}
### 1 New Memories List
${memoriesJson}

View File

@ -6,27 +6,27 @@
// ─── 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.
1.
2. "发现了什么关键线索""做了什么关键动作""修改了什么具体内容""遇到了什么具体报错"
3.
Before producing summaries, always perform the following internal reasoning:
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. 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. 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).
JSON [{...}]****
- "tool_call":
· tool pair [NEEDS_COMPRESS]+150/
exec({"command":"python3 -c 'import csv; ...200行脚本...'"}) "exec: 运行 Python xx/xx/xx.sh标明具体路径和文件脚本分析 sales_channels.csv 数据质量"
write_file({"path":"/root/app.py","content":"...5000字符..."}) "write_file: 写入 /root/app.py (Flask 应用主文件),大致内容是……"
· [NEEDS_COMPRESS]
- "summary": 200/
- "tool_call_id": tool_call_id
- "timestamp": +08:00ISO 8601
- "score"****: summary对于原文的可替代性0-1010summary越能替代原文
[Output Format Requirements]
You must output one and only one valid JSON array of objects [{...}]; each object MUST contain the following fields:
- "tool_call": a concise description of the tool call. Handling rules:
· 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.
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"
Example: write_file({"path":"/root/app.py","content":"...5000 chars..."}) "write_file: write /root/app.py (Flask app main file); its rough contents are ..."
· If not marked [NEEDS_COMPRESS], just briefly describe the tool and its parameters (the system will overwrite this with the original value).
- "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": the original tool_call_id (must be passed through verbatim).
- "timestamp": the original China Standard Time (+08:00) ISO 8601 timestamp (must be passed through verbatim).
- "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.
JSON `;
[Strict Rules]
Output only the raw JSON array. Never output your reasoning process or any other explanatory text.`;
// ─── Constants ───────────────────────────────────────────────────────────────
@ -53,7 +53,7 @@ export interface L1ToolPair {
export function buildL1UserPrompt(recentMessages: string, pairs: L1ToolPair[]): string {
const parts: string[] = [];
parts.push("## 最近的对话上下文(用于理解当前任务):");
parts.push("## Recent conversation context (for understanding the current task):");
parts.push(recentMessages);
parts.push("\n## Tool call/result pairs to summarize:");

View File

@ -6,25 +6,25 @@
// ─── System Prompt ───────────────────────────────────────────────────────────
export const L15_SYSTEM_PROMPT = `你是一个面向 AI 编码助手的"任务生命周期门神"。
JSON
export const L15_SYSTEM_PROMPT = `You are a "task lifecycle gatekeeper" for an AI coding assistant.
Your duty is to cross-analyze the three provided input sources, precisely assess the task state, and output a raw JSON object.
1. - recentMessages"继续排查""宣布完工(如:跑通了)""单轮闲聊问答""开启全新需求"
2. - currentMmd线 currentMmd Mermaid taskGoal statusdone/doing/todo summary done taskCompleted true doing bug false(currentMmd)
3. - availableMmdsisLongTask=true taskCompleted=true/ availableMmds taskGoal isContinuation=true
[Input Usage Guide (three-step reasoning chain you MUST follow)]
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. 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. 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
JSON
[Strict JSON Output Format]
Always output a valid raw JSON object in the following format:
{
"taskCompleted": boolean, // 当前任务是否已结束(如果 currentMmd 为 none这里必须填 true
"isLongTask": boolean, // 最新诉求是否是需要多步操作的复杂工程(普通技术问答、闲聊填 false
"isContinuation": boolean, // 是否在延续 availableMmds 中的历史任务
"continuationMmdFile": "string|null", // 若延续旧任务,精确填入 availableMmds 中的文件名(不含路径前缀),否则为 null
"newTaskLabel": "string|null" // 若是全新长任务生成简短标签≤30字符kebab-case如 "refactor-api"),否则为 null
"taskCompleted": boolean, // whether the current task has ended (if currentMmd is none, this MUST be true)
"isLongTask": boolean, // whether the latest request is a complex, multi-step engineering effort (ordinary technical Q&A or chit-chat → false)
"isContinuation": boolean, // whether this continues a historical task from availableMmds
"continuationMmdFile": "string|null", // if continuing an old task, fill in the exact filename from availableMmds (without path prefix); otherwise 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 ───────────────────────────────────────────────────────────────────
@ -58,9 +58,9 @@ export function buildL15UserPrompt(
): string {
const parts: string[] = [];
parts.push("## 1. 最近的对话上下文 (Recent 6 messages):");
parts.push("## 1. Recent conversation context (Recent 6 messages):");
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) {
parts.push(`**File:** ${currentMmd.filename}`);
@ -69,13 +69,13 @@ export function buildL15UserPrompt(
}
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
} 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) {
parts.push("(none - 暂无历史长任务)");
parts.push("(none - no historical long tasks yet)");
} else {
for (const m of metas) {
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");
}

View File

@ -6,43 +6,43 @@
// ─── System Prompt ───────────────────────────────────────────────────────────
export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓扑架构师与视觉叙事者。
LLM模型能看懂 Mermaid (flowchart TD) "过去""未来""雷区"
export const L2_SYSTEM_PROMPT = `You are an ultra-pragmatic AI task topology architect and visual storyteller.
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.
1.
2. ()status: blockedfail信息则不需要记录
3. summary150"得出了什么结论""发生了什么实质改变"
4. node_id
Token "认知锚点"使mmd形状来代表不同的节点逻辑
[High-Level Cognition & Topology Guide (your autonomy and minimalism principles)]
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. 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. 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. 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).
[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.
1. "领域" summary 150"发现死锁""依赖冲突""已修复"
2. 使线-->||线-.->||"依赖树""假设验证环"
3. (Token )
- replace ()
- write ()
Existing Mermaid content "L1: ..." replace MMD
[Highly Free Topology & Minimalism Rules]
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. 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. Dynamic updates (token minimalism):
- replace (incremental tweak): only when modifying existing nodes' status, timestamps, short text, or appending very few nodes.
- write (full rewrite): for major logic reshuffles, diagram restructuring, or initialization.
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.
线
1.NodeID["阶段名: 宏观动作简述<br/>status: done|doing|paused|blocked <br/>summary: 核心结论摘要<br/>Timestamp: ISO8601"]
2. 宿 tool_call_id node_mapping Node IDMMD里的每一个node都应该有源头的tool_call消息来源Node_id和tool_call_id是一对多的关系
3. mmd文件大小控制在4000字以内
[Strict Engineering Baselines]
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. 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. Use whatever consolidation methods you can to keep the updated mmd file under 4000 characters.
1. %%{ "taskGoal": "一句话总结此次任务的目标(可动态更新)", "progress0-100": "进度百分比严格点几乎确认完成再打到90+)", createdTime": "ISO时间", "updatedTime": "ISO时间" }%%updatedTime为node中的最新时间
2. Timestamp ISO
[Strict Timestamp & Metadata Rules]
1. Top metadata (required): %%{ "taskGoal": "one-sentence summary of this task's goal (may be updated dynamically)", "progress0-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. 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
Mermaid mmd_content replace_blocks content \`\`\`mermaid ... \`\`\` 代码块包裹起来。必须输出如下 JSON 结构:
[Strict JSON Output Format]
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",
"mmd_content": "完整的、带转义的 .mmd 代码,必须用 \`\`\`mermaid ... \`\`\` 包裹。(仅在 file_action 为 write 时填写,否则必须设为 null",
"file_action": "replace or write",
"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": [
{
"start_line": "需要更新范围的起始行号(整数,对应 Existing Mermaid content 中的 L 标号)",
"end_line": "需要更新范围的结束行号(整数,包含该行)。要在某行之前插入新内容而不删除任何行,将 start_line 设为该行号end_line 设为 start_line - 1",
"content": "替换后的新内容(不需要带行号前缀),必须用 \`\`\`mermaid ... \`\`\` 包裹"
"start_line": "starting line number of the range to update (integer, matching the L labels in Existing Mermaid content)",
"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": "the new replacement content (no line-number prefixes), wrapped in \`\`\`mermaid ... \`\`\`"
}
],
"node_mapping": {
@ -51,7 +51,7 @@ export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓
}
}
JSON `;
Output only the raw JSON object. Never include any explanation.`;
// ─── Types ───────────────────────────────────────────────────────────────────
@ -82,26 +82,26 @@ export function buildL2UserPrompt(opts: {
// History section
if (recentHistory) {
parts.push(`## 近期对话历史:\n${recentHistory}`);
parts.push(`## Recent conversation history:\n${recentHistory}`);
} else {
parts.push("## 近期对话历史:\n(无可用历史)");
parts.push("## Recent conversation history:\n(no history available)");
}
if (currentTurn) {
parts.push(`\n## 当前最新一轮:\n${currentTurn}`);
parts.push(`\n## Latest turn:\n${currentTurn}`);
}
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}`);
// Char count warning
if (charCount > 2500) {
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) {
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
@ -122,6 +122,6 @@ export function buildL2UserPrompt(opts: {
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");
}

View File

@ -7,27 +7,27 @@ const PARAMS_MAX_LEN = 500;
const RESULT_MAX_LEN = 2000;
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.
1.
2. "发现了什么关键线索""做了什么关键动作""修改了什么具体内容""遇到了什么具体报错"
3.
Before producing summaries, always perform the following internal reasoning:
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. 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. 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).
JSON [{...}]****
- "tool_call":
· tool pair [NEEDS_COMPRESS]+150/
exec({"command":"python3 -c 'import csv; ...200行脚本...'"}) "exec: 运行 Python xx/xx/xx.sh标明具体路径和文件脚本分析 sales_channels.csv 数据质量"
write_file({"path":"/root/app.py","content":"...5000字符..."}) "write_file: 写入 /root/app.py (Flask 应用主文件),大致内容是……"
· [NEEDS_COMPRESS]
- "summary": 200/
- "tool_call_id": tool_call_id
- "timestamp": ISO 8601
- "score"****: summary对于原文的可替代性0-1010summary越能替代原文
[Output Format Requirements]
You must output one and only one valid JSON array of objects [{...}]; each object MUST contain the following fields:
- "tool_call": a concise description of the tool call. Handling rules:
· 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.
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"
Example: write_file({"path":"/root/app.py","content":"...5000 chars..."}) "write_file: write /root/app.py (Flask app main file); its rough contents are ..."
· If not marked [NEEDS_COMPRESS], just briefly describe the tool and its parameters (the system will overwrite this with the original value).
- "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": the original tool_call_id (must be passed through verbatim).
- "timestamp": the original ISO 8601 timestamp (must be passed through verbatim).
- "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.
JSON `;
[Strict Rules]
Output only the raw JSON array. Never output your reasoning process or any other explanatory text.`;
/**
* Build the L1 user prompt for summarization.
@ -38,8 +38,8 @@ export function buildL1UserPrompt(
): string {
const parts: string[] = [];
parts.push("## 最近的对话上下文(用于理解当前任务):");
parts.push(recentContext || "(无可用上下文)");
parts.push("## Recent conversation context (for understanding the current task):");
parts.push(recentContext || "(no context available)");
parts.push("\n## Tool call/result pairs to summarize:");
for (let i = 0; i < pairs.length; i++) {

View File

@ -3,25 +3,25 @@
*/
import type { MmdMeta } from "../types.js";
export const L15_SYSTEM_PROMPT = `你是一个面向 AI 编码助手的"任务生命周期门神"。
JSON
export const L15_SYSTEM_PROMPT = `You are a "task lifecycle gatekeeper" for an AI coding assistant.
Your duty is to cross-analyze the three provided input sources, precisely assess the task state, and output a raw JSON object.
1. - recentMessages"继续排查""宣布完工(如:跑通了)""单轮闲聊问答""开启全新需求"
2. - currentMmd线 currentMmd Mermaid taskGoal statusdone/doing/todo summary done taskCompleted true doing bug false(currentMmd)
3. - availableMmdsisLongTask=true taskCompleted=true/ availableMmds taskGoal isContinuation=true
[Input Usage Guide (three-step reasoning chain you MUST follow)]
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. 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. 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
JSON
[Strict JSON Output Format]
Always output a valid raw JSON object in the following format:
{
"taskCompleted": boolean, // 当前任务是否已结束(如果 currentMmd 为 none这里必须填 true
"isLongTask": boolean, // 最新诉求是否是需要多步操作的复杂工程(普通技术问答、闲聊填 false
"isContinuation": boolean, // 是否在延续 availableMmds 中的历史任务
"continuationMmdFile": "string|null", // 若延续旧任务,精确填入 availableMmds 中的文件名(不含路径前缀),否则为 null
"newTaskLabel": "string|null" // 若是全新长任务生成简短标签≤30字符kebab-case如 "refactor-api"),否则为 null
"taskCompleted": boolean, // whether the current task has ended (if currentMmd is none, this MUST be true)
"isLongTask": boolean, // whether the latest request is a complex, multi-step engineering effort (ordinary technical Q&A or chit-chat → false)
"isContinuation": boolean, // whether this continues a historical task from availableMmds
"continuationMmdFile": "string|null", // if continuing an old task, fill in the exact filename from availableMmds (without path prefix); otherwise 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 {
filename: string;
@ -38,21 +38,21 @@ export function buildL15UserPrompt(
): string {
const parts: string[] = [];
parts.push("## 1. 最近的对话上下文 (Recent messages):");
parts.push("## 1. Recent conversation context (Recent messages):");
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) {
parts.push(`**File:** ${currentMmd.filename}`);
parts.push(`\n\`\`\`mermaid\n${currentMmd.content}\n\`\`\``);
} 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) {
parts.push("(none - 暂无历史长任务)");
parts.push("(none - no historical long tasks yet)");
} else {
for (const m of metas) {
const total = m.doneCount + m.doingCount + m.todoCount;
@ -75,7 +75,7 @@ export function buildL15UserPrompt(
}
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");
}

View File

@ -3,43 +3,43 @@
*/
import type { OffloadEntry } from "../types.js";
export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓扑架构师与视觉叙事者。
LLM模型能看懂 Mermaid (flowchart TD) "过去""未来""雷区"
export const L2_SYSTEM_PROMPT = `You are an ultra-pragmatic AI task topology architect and visual storyteller.
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.
1.
2. ()status: blockedfail信息则不需要记录
3. summary150"得出了什么结论""发生了什么实质改变"
4. node_id
Token "认知锚点"使mmd形状来代表不同的节点逻辑
[High-Level Cognition & Topology Guide (your autonomy and minimalism principles)]
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. 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. 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. 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).
[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.
1. "领域" summary 150"发现死锁""依赖冲突""已修复"
2. 使线-->||线-.->||"依赖树""假设验证环"
3. (Token )
- replace ()
- write ()
Existing Mermaid content "L1: ..." replace MMD
[Highly Free Topology & Minimalism Rules]
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. 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. Dynamic updates (token minimalism):
- replace (incremental tweak): only when modifying existing nodes' status, timestamps, short text, or appending very few nodes.
- write (full rewrite): for major logic reshuffles, diagram restructuring, or initialization.
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.
线
1.NodeID["阶段名: 宏观动作简述<br/>status: done|doing|paused|blocked <br/>summary: 核心结论摘要<br/>Timestamp: ISO8601"]
2. 宿 tool_call_id node_mapping Node IDMMD里的每一个node都应该有源头的tool_call消息来源Node_id和tool_call_id是一对多的关系
3. mmd文件大小控制在4000字以内
[Strict Engineering Baselines]
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. 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. Use whatever consolidation methods you can to keep the updated mmd file under 4000 characters.
1. %%{ "taskGoal": "一句话总结此次任务的目标(可动态更新)", "progress0-100": "进度百分比严格点几乎确认完成再打到90+)", createdTime": "ISO时间", "updatedTime": "ISO时间" }%%updatedTime为node中的最新时间
2. Timestamp ISO
[Strict Timestamp & Metadata Rules]
1. Top metadata (required): %%{ "taskGoal": "one-sentence summary of this task's goal (may be updated dynamically)", "progress0-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. 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
Mermaid mmd_content replace_blocks content \`\`\`mermaid ... \`\`\` 代码块包裹起来。必须输出如下 JSON 结构:
[Strict JSON Output Format]
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",
"mmd_content": "完整的、带转义的 .mmd 代码,必须用 \`\`\`mermaid ... \`\`\` 包裹。(仅在 file_action 为 write 时填写,否则必须设为 null",
"file_action": "replace or write",
"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": [
{
"start_line": "需要更新范围的起始行号(整数,对应 Existing Mermaid content 中的 L 标号)",
"end_line": "需要更新范围的结束行号(整数,包含该行)。要在某行之前插入新内容而不删除任何行,将 start_line 设为该行号end_line 设为 start_line - 1",
"content": "替换后的新内容(不需要带行号前缀),必须用 \`\`\`mermaid ... \`\`\` 包裹"
"start_line": "starting line number of the range to update (integer, matching the L labels in Existing Mermaid content)",
"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": "the new replacement content (no line-number prefixes), wrapped in \`\`\`mermaid ... \`\`\`"
}
],
"node_mapping": {
@ -48,8 +48,8 @@ export const L2_SYSTEM_PROMPT = `你是一个究极实用主义的 AI 任务拓
}
}
node_mapping Node ID MMD 使 ID MMD prefix "001-N1" ID "N1"
JSON `;
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").
Output only the raw JSON object. Never include any explanation.`;
/**
* Build the L2 user prompt for MMD generation.
@ -68,26 +68,26 @@ export function buildL2UserPrompt(opts: {
// History section
if (recentHistory) {
parts.push(`## 近期对话历史:\n${recentHistory}`);
parts.push(`## Recent conversation history:\n${recentHistory}`);
} else {
parts.push("## 近期对话历史:\n(无可用历史)");
parts.push("## Recent conversation history:\n(no history available)");
}
if (currentTurn) {
parts.push(`\n## 当前最新一轮:\n${currentTurn}`);
parts.push(`\n## Latest turn:\n${currentTurn}`);
}
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}`);
// Char count warning
if (charCount > 2500) {
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) {
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
@ -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("\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");
}

View 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: {}

View File

@ -81,7 +81,7 @@ export async function mergePage(
// locked → 跳过,保护用户手工编辑。
const oldParsed = parseFrontmatter(existingContent);
if (oldParsed.frontmatter.locked === true) {
return { action: "skip", reason: "目标页 locked跳过合并" };
return { action: "skip", reason: "target page is locked; skipping merge" };
}
const candParsed = parseFrontmatter(candidateContent);

View File

@ -87,7 +87,7 @@ export async function generateOverview(projectPath: string, llm: LlmClient): Pro
const wikiDir = join(projectPath, "wiki");
const briefs = collectBriefs(wikiDir);
if (briefs.length < 2) {
log.debug("页面太少,跳过 overview 生成", { pages: briefs.length });
log.debug("too few pages, skipping overview generation", { pages: briefs.length });
return false;
}

View File

@ -32,7 +32,7 @@ export interface PageForUpdate {
function formatExistingPages(existingPages: ExistingPageInfo[]): string {
return existingPages.length > 0
? 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")
: "(wiki is empty — this is the first source)";
}
@ -164,7 +164,7 @@ export function buildGeneratePrompt(args: {
const existingList =
existingPages.length > 0
? 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")
: "(wiki is empty — this is the first source)";

View File

@ -55,10 +55,18 @@ export class GitSourceFetcher implements ISourceFetcher {
}
validate(sourceUrl: string): void {
// 第一版:仅支持 public HTTPS 仓库SSH / 私有仓库鉴权见文档 005
if (!sourceUrl.startsWith("https://")) {
// GODCALL: plain http:// is allowed when KNOWLEDGE_ALLOW_HTTP is set —
// 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(
"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);

View File

@ -1,9 +1,9 @@
<!doctype html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8" />
<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" />
</head>
<body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -28,7 +28,7 @@ export default function App() {
if (auth === null) {
return (
<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>
);
}

View File

@ -233,7 +233,7 @@ export default function LoginGate({
// 并把下拉 placeholder 切成"加载失败",用户可刷新重试。
if (cancelled) return;
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 () => {
cancelled = true;
@ -243,12 +243,12 @@ export default function LoginGate({
async function submit(e?: React.FormEvent) {
e?.preventDefault();
if (!instanceId) {
setError('请选择记忆实例。');
setError('Please select a memory instance.');
return;
}
const key = userKey.trim();
if (!key) {
setError('请输入你的 user_keysk-mem-…)。');
setError('Please enter your user_key (sk-mem-…).');
return;
}
setSubmitting(true);
@ -256,12 +256,12 @@ export default function LoginGate({
try {
const { valid, user } = await authVerifyApi.verify(instanceId, key);
if (!valid) {
setError('user_key 无效或已吊销,请确认后重新输入。');
setError('user_key is invalid or has been revoked. Please check it and re-enter.');
setSubmitting(false);
return;
}
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);
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="flex items-center gap-2.5 px-6 py-5">
<img src="/logo.png" alt="Memory Hub" className="h-8 w-8" />
<span className="text-[15px] font-semibold text-white/90 tracking-wide">Memory Hub</span>
<img src="/logo.png" alt="GODCALL" className="h-8 w-8" />
<span className="text-[15px] font-semibold text-white/90 tracking-wide">GODCALL</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center px-8">
<HeroIllustration />
<h2 className="mt-8 text-xl font-semibold text-white/90 tracking-wide">
TencentDB Memory Hub
GODCALL
</h2>
<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>
</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="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">
Memory Hub
GODCALL
</span>
</div>
<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">
user_key
Select a memory instance and enter your user_key to log in.
</p>
<form onSubmit={submit} className="mt-8 _tdai-login-form">
@ -342,7 +342,7 @@ export default function LoginGate({
setError(null);
}}
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 }))}
/>
@ -357,13 +357,13 @@ export default function LoginGate({
setError(null);
}}
onKeyDown={onKeyDown}
placeholder="user_key,如 sk-mem-xxxxxxxxxxxxxxxx"
placeholder="user_key, e.g. sk-mem-xxxxxxxxxxxxxxxx"
autoComplete="current-password"
disabled={submitting}
rules={false}
/>
<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>
@ -375,7 +375,7 @@ export default function LoginGate({
loading={submitting}
disabled={submitting || !userKey.trim() || !instanceId}
>
{submitting ? '登录中…' : '登录'}
{submitting ? 'Logging in…' : 'Log in'}
</Button>
</form>
</div>

View File

@ -41,29 +41,29 @@ const RESOURCE_MODULES: ResourceModule[] = [
{
id: 'wiki',
paramKey: 'llm_wiki.enabled',
label: 'Wiki 知识库',
desc: '关闭后仅停止工具注入',
label: 'Wiki knowledge base',
desc: 'Disabling only stops tool injection',
icon: <BooksIcon size={16} />,
},
{
id: 'code',
paramKey: 'code_graph.enabled',
label: 'Code_Graph',
desc: '关闭后仅停止工具注入',
desc: 'Disabling only stops tool injection',
icon: <CodeIcon size={16} />,
},
{
id: 'skill',
paramKey: 'skill.enabled',
label: 'Skill 技能',
desc: '关闭后工具注入与新技能抽取均停止',
label: 'Skill',
desc: 'Disabling stops both tool injection and new skill extraction',
icon: <ToolsIcon size={16} />,
},
{
id: 'chat_memory',
paramKey: 'chat_memory.enabled',
label: 'Chat_Memory',
desc: '关闭后工具注入与新对话写入均停止',
desc: 'Disabling stops both tool injection and new conversation writes',
icon: <ChatIcon size={16} />,
},
];
@ -122,19 +122,19 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
setError('');
try {
await userConfigApi.setAssetCapability(mod.paramKey, next);
tea.notify.success(`${mod.label} ${next ? '开启' : '关闭'}`);
tea.notify.success(`${mod.label} has been ${next ? 'enabled' : 'disabled'}`);
} catch (e) {
setEnabled((prev) => ({ ...prev, [mod.id]: previous }));
const msg = e instanceof Error ? e.message : String(e);
setError(msg);
tea.notify.error(`保存失败:${msg}`);
tea.notify.error(`Save failed: ${msg}`);
} finally {
setSavingKey(null);
}
}
return (
<Modal visible caption="设置 · 权限管理" size="m" onClose={onClose}>
<Modal visible caption="Settings · Permissions" size="m" onClose={onClose}>
<Modal.Body>
{/*
tab <Tabs>+<TabPanel> 线
@ -146,13 +146,13 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
<div>
<div style={{ paddingTop: 4 }}>
<Text theme="label" style={{ display: 'block', marginBottom: 8 }}>
Resource module toggles
</Text>
<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>
{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 }}>
{RESOURCE_MODULES.map((mod) => (
@ -181,11 +181,11 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) {
{mod.label}
</Text>
{savingKey === mod.paramKey ? (
<Tag theme="warning" variant="soft" size="sm"></Tag>
<Tag theme="warning" variant="soft" size="sm">Saving</Tag>
) : 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>
<Text theme="weak" style={{ fontSize: 12, marginTop: 2, display: 'block' }}>

View File

@ -39,18 +39,18 @@ export interface PageMeta {
}
export const PAGE_META: Record<PageId, PageMeta> = {
workbench_board: { id: 'workbench_board', label: '任务看板', desc: 'Task 列表 / 创建 / 详情', group: '工作台', order: 0, affix: true },
wiki: { id: 'wiki', label: 'Wiki 知识库', desc: '来源 / 图谱 / 页面 / 搜索', group: '资产管理', order: 2 },
code: { id: 'code', label: 'Code_Graph', desc: '仓库 / 索引 / 搜索 / 探索', group: '资产管理', order: 3 },
skills: { id: 'skills', label: 'Skill 技能', desc: '全部 / 团队池 / Agent 资产', group: '资产管理', order: 4 },
chat_memory: { id: 'chat_memory', label: 'Chat_Memory', desc: 'L0L3 分层记忆资产', group: '资产管理', order: 5 },
team_members: { id: 'team_members', label: '成员管理', desc: 'Team 成员 / 用户 / 角色', group: '组织与权限', order: 0 },
team_agents: { id: 'team_agents', label: 'Agents 管理', desc: 'Agent / 可配置范围 / 固定资产', group: '组织与权限', order: 1 },
api_keys: { id: 'api_keys', label: 'API Key', desc: '管理你的 API Key用于外部客户端接入', group: '组织与权限', order: 2 },
workbench_board: { id: 'workbench_board', label: 'Task board', desc: 'Task list / create / details', group: 'Workbench', order: 0, affix: true },
wiki: { id: 'wiki', label: 'Wiki knowledge base', desc: 'Sources / graph / pages / search', group: 'Assets', order: 2 },
code: { id: 'code', label: 'Code_Graph', desc: 'Repos / index / search / explore', group: 'Assets', order: 3 },
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 layered memory assets', group: 'Assets', order: 5 },
team_members: { id: 'team_members', label: 'Member management', desc: 'Team members / users / roles', group: 'Org & permissions', order: 0 },
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: '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 */
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> = {
: (
'Workbench': (
<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="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" />
</svg>
),
: (
'Org & permissions': (
<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" />
<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" />
</svg>
),
: (
'Assets': (
<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="M3 12l9 5 9-5" />

View File

@ -107,7 +107,7 @@ export function ConsoleLayout() {
for (const meta of Object.values(PAGE_META)) {
// admin 角色 → 跳过所有「资源管理」分组下的项
if (userRole === 'admin' && meta.group === '资源管理') continue;
if (userRole === 'admin' && meta.group === 'Resource management') continue;
// reviewer → 跳过「成员管理」member 可见,但新建/删除成员/Team 按钮在组件内按角色收敛)
if (userRole === 'reviewer' && meta.id === 'team_members') continue;
const list = byGroup.get(meta.group) ?? [];
@ -124,8 +124,8 @@ export function ConsoleLayout() {
}, [userRole]);
// 「工作台」分组只有任务看板一项,置顶展示为独立入口,不显示分组标题
const pinnedGroup = menuGroups.find((g) => g.title === '工作台');
const restGroups = menuGroups.filter((g) => g.title !== '工作台');
const pinnedGroup = menuGroups.find((g) => g.title === 'Workbench');
const restGroups = menuGroups.filter((g) => g.title !== 'Workbench');
const renderMenuItem = (item: PageMeta) => {
const isActive = activePage === item.id;

View File

@ -68,14 +68,14 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
<button
type="button"
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'}`}>
{(active?.name ?? '?').slice(0, 1).toUpperCase()}
</span>
<span className="_memory-team-switcher-meta">
<span className="_memory-team-switcher-name">{active?.name ?? '选择 team'}</span>
<span className="_memory-team-switcher-id">{active?.team_id ?? '未选择'}</span>
<span className="_memory-team-switcher-name">{active?.name ?? 'Select a team'}</span>
<span className="_memory-team-switcher-id">{active?.team_id ?? 'Not selected'}</span>
</span>
<ChevronDownIcon size={12} className="_memory-team-switcher-chevron" />
</button>
@ -84,20 +84,20 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
{(close) => (
<div className="_memory-team-switcher-panel">
<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">
Assets are independent across teams. Switching will show that team's data on the current page.
</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">
{myTeams.length === 0 ? (
<div className="_memory-team-switcher-empty">
{userRole === 'admin'
? '暂无 team。点击下方「新建团队」创建。'
: '你还没有被加入任何 team。请联系管理员将你加入团队。'}
? 'No teams yet. Click "New team" below to create one.'
: "You haven't been added to any team yet. Contact an admin to be added."}
</div>
) : (
<List type="plain" split="divide" className="_memory-team-switcher-list">
@ -115,7 +115,7 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
</span>
<span className="_memory-team-switcher-item-meta">
<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>
{isActive && <CheckIcon size={16} className="_memory-team-switcher-item-check" />}
</List.Item>
@ -133,22 +133,22 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
size="full"
value={newTeamName}
onChange={setNewTeamName}
placeholder="团队名称(必填)"
placeholder="Team name (required)"
/>
<Input
size="full"
value={newTeamDesc}
onChange={setNewTeamDesc}
placeholder="团队描述(选填)"
placeholder="Team description (optional)"
/>
<div className="_memory-team-switcher-create-actions">
<Button onClick={resetCreateForm}></Button>
<Button onClick={resetCreateForm}>Cancel</Button>
<Button type="primary"
loading={creating}
disabled={!newTeamName.trim() || creating}
onClick={handleCreate}
>
Create
</Button>
</div>
</div>
@ -158,7 +158,7 @@ export function TeamSwitcher({ userRole }: { userRole: TeamRole | null }) {
onClick={() => setShowCreateTeam(true)}
>
<AddIcon size={14} />
New team
</Button>
)}
</div>

View File

@ -31,23 +31,23 @@ export function GlobalHeader({
{/* 左侧:品牌 + 团队切换器 */}
<div className="_memory-global-header-left">
<div className="_memory-global-header-brand">
<img src="/logo.png" alt="Memory Hub" className="_memory-global-header-logo" />
<span className="_memory-global-header-brand-text">Memory Hub</span>
<img src="/logo.png" alt="GODCALL" className="_memory-global-header-logo" />
<span className="_memory-global-header-brand-text">GODCALL</span>
</div>
<TeamSwitcher userRole={userRole} />
</div>
{/* 右侧:同步状态 + 用户菜单 */}
<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" />
Live sync
</span>
<button
type="button"
className="_memory-global-header-icon-btn"
title="设置"
title="Settings"
onClick={() => setSettingsOpen(true)}
>
<SettingIcon size={16} />
@ -72,7 +72,7 @@ export function GlobalHeader({
setProfileOpen(true);
}}
>
My profile
</List.Item>
<List.Item
onClick={() => {
@ -80,7 +80,7 @@ export function GlobalHeader({
onLogout();
}}
>
退
Log out
</List.Item>
</List>
)}
@ -88,18 +88,18 @@ export function GlobalHeader({
</div>
{profileOpen && currentUserId && (
<Modal visible caption="我的资料" size="s" onClose={() => setProfileOpen(false)}>
<Modal visible caption="My profile" size="s" onClose={() => setProfileOpen(false)}>
<Modal.Body>
<dl className="_memory-profile-details">
<div><dt></dt><dd>{currentUser}</dd></div>
<div><dt>Username</dt><dd>{currentUser}</dd></div>
<div>
<dt>User ID</dt>
<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>
</dl>
</Modal.Body>
<Modal.Footer><Button onClick={() => setProfileOpen(false)}></Button></Modal.Footer>
<Modal.Footer><Button onClick={() => setProfileOpen(false)}>Close</Button></Modal.Footer>
</Modal>
)}

View File

@ -5,69 +5,69 @@ export interface ErrorEnvelopeLike {
}
const ERROR_CODE_MESSAGES: Record<string, string> = {
UNAUTHORIZED: '登录状态已失效,请重新登录。',
INVALID_USER_KEY: '用户密钥无效或已失效,请重新登录。',
MISSING_USER_KEY: '缺少用户密钥,请重新登录。',
MISSING_INSTANCE_ID: '缺少实例信息,请重新选择实例后重试。',
INVALID_INSTANCE: '实例配置无效,请检查当前选择的实例。',
NOT_TEAM_MEMBER: '你不是该团队成员,无法执行此操作。',
PERMISSION_DENIED: '没有权限执行此操作。',
FORBIDDEN: '没有权限执行此操作。',
NOT_FOUND: '资源不存在或已被删除。',
ALREADY_EXISTS: '资源已存在,请勿重复创建。',
MEMBER_ALREADY_EXISTS: '该用户已是团队成员,无需重复添加。',
CONFLICT: '资源状态已变化,请刷新后重试。',
KERNEL_UNAVAILABLE: '内核服务不可用,请稍后重试。',
UPSTREAM_ERROR: '上游服务调用失败,请稍后重试。',
UNKNOWN_META_ACTION: '当前接口暂不支持,请刷新页面或联系管理员。',
NOT_IN_SCOPE: '该能力当前暂未开放。',
UNAUTHORIZED: 'Your session has expired. Please log in again.',
INVALID_USER_KEY: 'Your user key is invalid or has expired. Please log in again.',
MISSING_USER_KEY: 'Missing user key. Please log in again.',
MISSING_INSTANCE_ID: 'Missing instance information. Please reselect an instance and retry.',
INVALID_INSTANCE: 'Invalid instance configuration. Please check the currently selected instance.',
NOT_TEAM_MEMBER: 'You are not a member of this team and cannot perform this action.',
PERMISSION_DENIED: 'You do not have permission to perform this action.',
FORBIDDEN: 'You do not have permission to perform this action.',
NOT_FOUND: 'The resource does not exist or has been deleted.',
ALREADY_EXISTS: 'The resource already exists. Please do not create it again.',
MEMBER_ALREADY_EXISTS: 'This user is already a team member and does not need to be added again.',
CONFLICT: 'The resource state has changed. Please refresh and try again.',
KERNEL_UNAVAILABLE: 'The kernel service is unavailable. Please try again later.',
UPSTREAM_ERROR: 'The upstream service call failed. Please try again later.',
UNKNOWN_META_ACTION: 'This endpoint is not currently supported. Please refresh the page or contact an administrator.',
NOT_IN_SCOPE: 'This capability is not currently available.',
MISSING_TEAM_ID: '缺少团队信息,请重新选择团队。',
MISSING_AGENT_ID: '缺少 Agent 信息,请重新选择 Agent。',
AGENT_NOT_FOUND: 'Agent 不存在或已被删除。',
NOT_YOUR_AGENT: '只能操作你自己创建的 Agent。',
AGENT_NOT_IN_TEAM: '该 Agent 不属于当前团队。',
MISSING_TASK_ID: '缺少 Task 信息,请重新选择 Task。',
MISSING_TEAM_ID: 'Missing team information. Please reselect a team.',
MISSING_AGENT_ID: 'Missing agent information. Please reselect an agent.',
AGENT_NOT_FOUND: 'The agent does not exist or has been deleted.',
NOT_YOUR_AGENT: 'You can only operate on agents you created yourself.',
AGENT_NOT_IN_TEAM: 'This agent does not belong to the current team.',
MISSING_TASK_ID: 'Missing task information. Please reselect a task.',
MISSING_ASSET_ID: '缺少资产 ID。',
ASSET_NOT_FOUND: '资产不存在或已被删除。',
ASSET_NOT_SHARED: '该资产尚未共享到团队,不能分配给其它 Agent。',
ASSET_TYPE_MISMATCH: '资产类型不匹配,请刷新后重试。',
MISSING_BLOCK_ID: '缺少记忆资产信息。',
BLOCK_NOT_FOUND: '记忆资产不存在或已被删除。',
NOT_CHAT_MEMORY: '当前资产不是 Chat Memory。',
TEAM_MISMATCH: '资源不属于当前团队,请刷新后重试。',
INVALID_SCOPE: '可见范围无效。',
MISSING_ASSET_ID: 'Missing asset ID.',
ASSET_NOT_FOUND: 'The asset does not exist or has been deleted.',
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. Please refresh and try again.',
MISSING_BLOCK_ID: 'Missing memory asset information.',
BLOCK_NOT_FOUND: 'The memory asset does not exist or has been deleted.',
NOT_CHAT_MEMORY: 'This asset is not a Chat Memory.',
TEAM_MISMATCH: 'The resource does not belong to the current team. Please refresh and try again.',
INVALID_SCOPE: 'Invalid visibility scope.',
CANNOT_ALLOCATE_SELF_CHAT_MEMORY: '不能把该 Agent 自己的记忆再分配给自己。',
CANNOT_UNBIND_SELF_CHAT_MEMORY: '不能解绑 Agent 自己的记忆。',
ALREADY_ALLOCATED: '这条资产已经分配给该 Agent无需重复分配。',
IMPORT_LIMIT_EXCEEDED: '该 Agent 最多只能借入 2 条其它 Agent 的记忆。',
CANNOT_ALLOCATE_SELF_CHAT_MEMORY: 'You cannot assign this agent\'s own memory back to itself.',
CANNOT_UNBIND_SELF_CHAT_MEMORY: 'You cannot unbind an agent\'s own memory.',
ALREADY_ALLOCATED: 'This asset has already been assigned to this agent and does not need to be assigned again.',
IMPORT_LIMIT_EXCEEDED: 'This agent can borrow at most 2 memories from other agents.',
// 内核 canBindAsset/permission-checker 判定失败:常见场景是 asset 被 owner 切私密后
// 其他成员再对它做 read / bind / update 类操作。
ASSET_PRIVATE_INACCESSIBLE: '该资产已被 owner 设为私密,你无权访问。',
ASSET_NOT_BINDABLE: '该资产的可见范围不允许绑定到此 Agent。请让 owner 将它设为团队可见后重试。',
ASSET_PRIVATE_INACCESSIBLE: 'This asset has been set to private by its owner. You do not have access to it.',
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: '标题不能为空且不能超过长度限制。',
MISSING_MESSAGES: '缺少对话消息。',
TOO_MANY_MESSAGES: '一次最多导入 100 条消息。',
NO_VALID_MESSAGES: '没有可导入的有效消息。',
INVALID_TITLE: 'The title cannot be empty and must not exceed the length limit.',
MISSING_MESSAGES: 'Missing conversation messages.',
TOO_MANY_MESSAGES: 'You can import at most 100 messages at a time.',
NO_VALID_MESSAGES: 'There are no valid messages to import.',
MISSING_WIKI_ID: '缺少 Wiki 信息。',
WIKI_NOT_FOUND: 'Wiki 不存在或已被删除。',
WIKI_EMPTY_NO_SOURCES: 'Wiki 还没有上传源文件,请先上传 .md 文件后再抽取。',
MISSING_FILES: '请至少上传一个文件。',
TOO_MANY_FILES: '上传文件数量超过限制(最多 10 个),请分批上传。',
FILE_TOO_LARGE: '单个文件不能超过 512KB请精简后再上传。',
TOTAL_TOO_LARGE: '单次上传总量不能超过 5MB请分批上传。',
MISSING_CODE_GRAPH_ID: '缺少 CodeGraph 信息。',
CODE_GRAPH_NOT_FOUND: 'CodeGraph 不存在或已被删除。',
KNOWLEDGE_NOT_FOUND: '知识库资源不存在或已被删除。',
MISSING_WIKI_ID: 'Missing wiki information.',
WIKI_NOT_FOUND: 'The wiki does not exist or has been deleted.',
WIKI_EMPTY_NO_SOURCES: 'This wiki has no uploaded source files yet. Please upload .md files before extracting.',
MISSING_FILES: 'Please upload at least one file.',
TOO_MANY_FILES: 'Too many files uploaded (max 10). Please upload in batches.',
FILE_TOO_LARGE: 'A single file cannot exceed 512KB. Please trim it down before uploading.',
TOTAL_TOO_LARGE: 'The total upload size cannot exceed 5MB. Please upload in batches.',
MISSING_CODE_GRAPH_ID: 'Missing CodeGraph information.',
CODE_GRAPH_NOT_FOUND: 'The CodeGraph does not exist or has been deleted.',
KNOWLEDGE_NOT_FOUND: 'The knowledge base resource does not exist or has been deleted.',
INVALID_ARGUMENT: '请求参数不正确,请检查输入后重试。',
VALIDATION_ERROR: '请求参数不正确,请检查输入后重试。',
RATE_LIMITED: '请求过于频繁,请稍后重试。',
INTERNAL_ERROR: '服务内部错误,请稍后重试。',
INVALID_ARGUMENT: 'The request parameters are invalid. Please check your input and try again.',
VALIDATION_ERROR: 'The request parameters are invalid. Please check your input and try again.',
RATE_LIMITED: 'Too many requests. Please try again later.',
INTERNAL_ERROR: 'Internal server error. Please try again later.',
};
const MESSAGE_PATTERNS: Array<[RegExp, string]> = [
@ -79,9 +79,9 @@ const MESSAGE_PATTERNS: Array<[RegExp, string]> = [
// 注asset_not_bindable / visibility_restricted 在 PRIORITY_MESSAGE_PATTERNS 里前置匹配,
// 因为它们会被 permission_denied 前缀吞掉。
[/permission[_\s-]?denied/i, ERROR_CODE_MESSAGES.PERMISSION_DENIED],
[/fetch failed|networkerror|failed to fetch/i, '网络请求失败,请检查服务是否可用后重试。'],
[/timeout|aborted/i, '请求超时,请稍后重试。'],
[/empty .* response/i, '服务返回为空,请稍后重试。'],
[/fetch failed|networkerror|failed to fetch/i, 'Network request failed. Please check whether the service is available and try again.'],
[/timeout|aborted/i, 'The request timed out. Please try again later.'],
[/empty .* response/i, 'The service returned an empty response. Please try again later.'],
[/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 === 404) return ERROR_CODE_MESSAGES.NOT_FOUND;
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 {
@ -207,5 +207,5 @@ export function getErrorMessage(err: unknown): string {
}
if (err instanceof Error) return formatApiErrorMessage({ message: err.message, fallback: err.message });
if (typeof err === 'string') return formatApiErrorMessage({ message: err, fallback: err });
return '操作失败,请稍后重试。';
return 'Operation failed. Please try again later.';
}

View File

@ -258,17 +258,17 @@ async function listAgentFixedKnowledge(agentId: string): Promise<KnowledgeFixedI
// ========================= Wiki API =========================
export function wikiStageLabel(status: WikiDetail['status'], internalStatus?: string | null): string {
if (status === 'missing') return '已丢失';
if (status === 'pending') return '排队中';
if (status === 'ready') return '已完成';
if (status === 'failed') return '失败';
if (status === 'draft') return '待加工';
if (status === 'missing') return 'Lost';
if (status === 'pending') return 'Queued';
if (status === 'ready') return 'Completed';
if (status === 'failed') return 'Failed';
if (status === 'draft') return 'Awaiting processing';
const map: Record<string, string> = {
scanning: '扫描源文档',
ingesting: '抽取文档内容',
'rebuilding-index': '重建索引',
scanning: 'Scanning source documents',
ingesting: 'Extracting document content',
'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 {
@ -321,7 +321,7 @@ export const knowledgeApi = {
/** 触发 ingest 后轮询 wiki/get用真实 status/internal_status 驱动进度展示。 */
ingestWithPolling: async (wikiId: string, callbacks: IngestStreamCallbacks, _teamId: string): Promise<void> => {
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 {
await knowledgeApi.wiki.ingest(wikiId);
} catch (err: any) {
@ -335,27 +335,27 @@ export const knowledgeApi = {
const detail = await knowledgeApi.wiki.get(wikiId);
const stage = wikiStageLabel(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?.({
type: 'file_done',
detail: `${attempt} 次检查:${stage}${pageHint}`,
detail: `Check ${attempt}: ${stage}${pageHint}`,
done,
total: 100,
ts: Date.now(),
});
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;
callbacks.onComplete?.({ total: count, ingested: count });
return;
}
if (detail.status === 'failed') {
callbacks.onError?.(detail.sync_error || '抽取失败');
callbacks.onError?.(detail.sync_error || 'Extraction failed');
return;
}
}
callbacks.onError?.('抽取超时,请稍后刷新查看最新状态');
callbacks.onError?.('Extraction timed out. Refresh later to see the latest status.');
} catch (err: any) {
callbacks.onError?.(err.message || String(err));
}
@ -381,7 +381,7 @@ export const knowledgeApi = {
'/wiki/page/read', { wiki_id: wikiId, refs: [path] }
);
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 ?? '' };
},
@ -498,7 +498,7 @@ export async function pollWikiStatus(wikiId: string, maxAttempts = 30, intervalM
if (detail.status === 'ready' || detail.status === 'failed') return detail;
await new Promise(r => setTimeout(r, intervalMs));
}
throw new Error(`Wiki ${wikiId} ingest 超时`);
throw new Error(`Wiki ${wikiId} ingest timed out`);
}
/** 轮询 code-graph sync 状态 */
@ -508,5 +508,5 @@ export async function pollCodeGraphStatus(codeGraphId: string, maxAttempts = 30,
if (detail.status === 'ready' || detail.status === 'failed') return detail;
await new Promise(r => setTimeout(r, intervalMs));
}
throw new Error(`CodeGraph ${codeGraphId} sync 超时`);
throw new Error(`CodeGraph ${codeGraphId} sync timed out`);
}

View File

@ -117,7 +117,7 @@ async function skillCall<T>(action: string, body: Record<string, unknown>): Prom
body: JSON.stringify(stripEmpty(body)),
});
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();
let envelope: SkillEnvelope<T>;

View File

@ -53,8 +53,8 @@ export const tea = {
return Modal.confirm({
message: opts.message,
description: opts.description,
okText: opts.okText ?? '确认',
cancelText: opts.cancelText ?? '取消',
okText: opts.okText ?? 'Confirm',
cancelText: opts.cancelText ?? 'Cancel',
});
},
@ -87,7 +87,7 @@ export const tea = {
: `request_id: ${input.requestId}`
: input.detail;
notification.error({
title: input.title ?? '操作失败',
title: input.title ?? 'Operation failed',
description: desc,
});
return;
@ -99,13 +99,13 @@ export const tea = {
? `${friendly}\nrequest_id: ${requestId}`
: friendly;
notification.error({
title: '操作失败',
title: 'Operation failed',
description: desc,
});
},
warning: (msg: string) =>
notification.warning({
title: '提示',
title: 'Notice',
description: msg,
}),
info: (msg: string) =>
@ -129,9 +129,9 @@ export const tea = {
*/
confirmDelete: (name: string, detail?: string) =>
Modal.confirm({
message: `确认删除「${name}」?`,
description: detail ?? '删除后不可恢复。',
okText: '删除',
cancelText: '取消',
message: `Delete "${name}"?`,
description: detail ?? 'This cannot be undone.',
okText: 'Delete',
cancelText: 'Cancel',
}),
};

View File

@ -14,9 +14,9 @@ export function AdminResourceLock() {
<Card className="_memory-admin-lock-card">
<Card.Body>
<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">
Admin Team使
Admin accounts are currently for organization management only (creating teams, adding members). Use a regular member account for resource management.
</div>
</Card.Body>
</Card>

View File

@ -22,7 +22,7 @@ export type AllocateAssetType = 'skill' | 'llm_wiki' | 'code_graph' | 'chat_memo
const TYPE_LABEL: Record<AllocateAssetType, string> = {
skill: 'Skill',
llm_wiki: 'Wiki',
code_graph: '代码图谱',
code_graph: 'Code graph',
chat_memory: 'Memory'
};
@ -49,14 +49,14 @@ export default function AllocateAssetDialog(props: {
async function submit(): Promise<void> {
if (!agentId) {
setError('请选择 agent。');
setError('Please select an agent.');
return;
}
setError(null);
setSubmitting(true);
try {
await props.onAllocate(agentId);
tea.notify.success(`已分配「${props.assetLabel}」→ ${agentId}`);
tea.notify.success(`Allocated "${props.assetLabel}" to ${agentId}`);
props.onClose();
} catch (err) {
tea.notify.error(err);
@ -66,15 +66,15 @@ export default function AllocateAssetDialog(props: {
}
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>
<Form>
{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.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.Item>
<Form.Item label="Agent" required>
@ -82,7 +82,7 @@ export default function AllocateAssetDialog(props: {
size="full"
value={agentId}
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}` }))}
disabled={props.agents.length === 0}
/>
@ -91,8 +91,8 @@ export default function AllocateAssetDialog(props: {
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId} loading={submitting}></Button>
<Button onClick={props.onClose} disabled={submitting}></Button>
<Button type="primary" onClick={() => void submit()} disabled={submitting || !agentId} loading={submitting}>Allocate</Button>
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -42,8 +42,8 @@ export interface AssetScopeItem {
}
const SCOPE_OPTIONS: Array<{ value: AssetConfigScope; label: string }> = [
{ value: 'team', label: '团队内可配置' },
{ value: 'private', label: '仅自己私有' }
{ value: 'team', label: 'Configurable by team' },
{ value: 'private', label: 'Private (owner only)' }
];
export default function AssetScopeManager({
@ -68,22 +68,22 @@ export default function AssetScopeManager({
return (
<div className="_memory-asset-scope">
<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">
owner {label}
Each owner can manage their own {label}: choose
<span className="_memory-asset-scope-alert-em">
<UsergroupIcon size={12} />
<UsergroupIcon size={12} /> Configurable by team
</span>
(any team member can change it) or
<span className="_memory-asset-scope-alert-em">
<LockOnIcon size={12} />
<LockOnIcon size={12} /> Private (owner only)
</span>
owner
(only you can change it). Only the asset owner and team admins can switch this.
</div>
</Alert>
{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">
{items.map((item) => {
@ -103,10 +103,10 @@ export default function AssetScopeManager({
{effectiveOwner ? (
<Text theme="weak" className="_memory-asset-scope-item-owner">
owner <span className="_memory-asset-scope-item-owner-id">@{effectiveOwner}</span>
{ownerIsMe && <Text theme="primary"> </Text>}
{ownerIsMe && <Text theme="primary"> (you)</Text>}
</Text>
) : (
<Text theme="weak" className="_memory-asset-scope-item-owner"></Text>
<Text theme="weak" className="_memory-asset-scope-item-owner">Unowned</Text>
)}
</div>
{item.meta && (
@ -127,7 +127,7 @@ export default function AssetScopeManager({
/>
) : (
<Tag theme={scope === 'private' ? 'default' : 'success'} size="sm">
{scope === 'private' ? '仅自己私有' : '团队内可配置'}
{scope === 'private' ? 'Private (owner only)' : 'Configurable by team'}
</Tag>
)}
</li>

View File

@ -71,8 +71,8 @@ function isValidGitHttpUrl(raw: string): boolean {
type ScopeTab = 'team' | 'fixed';
const SCOPE_LABELS: Record<ScopeTab, string> = {
team: '团队 Code 池',
fixed: 'Agent 资产',
team: 'Team code pool',
fixed: 'Agent assets',
};
/**
@ -84,7 +84,7 @@ function CodeOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
return (
<span title={`Owner: ${userId}`}>
@{name || userId}
{userId === currentUserId && <span className="_codelist-card-meta-you"></span>}
{userId === currentUserId && <span className="_codelist-card-meta-you">(you)</span>}
</span>
);
}
@ -92,18 +92,18 @@ function CodeOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
// 状态 → Tea Tag 语义主题映射soft 变体),对齐 Memory 的 statusTheme。
function statusLabel(s: string) {
const map: Record<string, [string, 'default' | 'success' | 'warning' | 'error']> = {
ready: ['就绪', 'success'],
pending: ['排队中', 'warning'],
processing: ['构建中', 'warning'],
failed: ['失败', 'error'],
cloning: ['克隆中', 'warning'],
indexing: ['索引中', 'warning'],
syncing: ['同步中', 'warning'],
error: ['错误', 'error'],
missing: ['已丢失', 'error'],
ready: ['Ready', 'success'],
pending: ['Queued', 'warning'],
processing: ['Building', 'warning'],
failed: ['Failed', 'error'],
cloning: ['Cloning', 'warning'],
indexing: ['Indexing', 'warning'],
syncing: ['Syncing', 'warning'],
error: ['Error', 'error'],
missing: ['Lost', 'error'],
};
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>;
}
@ -161,7 +161,7 @@ export default function CodeSourcesPanel() {
const items = await knowledgeApi.code.agentFixed(agentFilter);
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
} catch (e: any) {
tea.notify.error(e?.message || '加载固定资产失败');
tea.notify.error(e?.message || 'Failed to load fixed assets');
setFixedBoundIds(new Set());
}
}, [agentFilter]);
@ -287,7 +287,7 @@ export default function CodeSourcesPanel() {
// callback S2S 是主力,这里只是兜底,但失败要可见)
const msg = e?.message || String(e);
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);
@ -323,19 +323,19 @@ export default function CodeSourcesPanel() {
async function handleUnbindCode(codeGraphId: string) {
if (!agentFilter) return;
const ok = await tea.confirm({
message: '确认解绑该代码图谱?',
description: '将从当前 agent 移除该代码图谱绑定。',
okText: '解绑',
message: 'Unbind this code graph?',
description: 'This removes the code graph binding from the current agent.',
okText: 'Unbind',
});
if (!ok) return;
try {
await knowledgeApi.code.unbind(codeGraphId, agentFilter);
tea.notify.success('已解绑');
tea.notify.success('Unbound');
if (selectedCodeAsset?.cgId === codeGraphId) setSelectedCodeAsset(null);
await fetchFixedBindings();
await fetchSources();
} 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;
// 防御性校验:按钮已按 validUrl 禁用,这里再挡一层防止绕过
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;
}
setSubmitting(true);
@ -353,7 +353,7 @@ export default function CodeSourcesPanel() {
setShowRegister(false); setFormRepo(''); setFormBranch('main');
setScopeTab('team');
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();
} catch (e: any) { tea.notify.error(e); }
finally { setSubmitting(false); }
@ -368,8 +368,8 @@ export default function CodeSourcesPanel() {
const source = sources.find(s => s.code_graph_id === cgId);
if (!source) return;
const ok = await tea.confirm({
message: `确定要删除仓库「${source.repo_name || source.repo_url} (${source.branch})」吗?`,
okText: '删除',
message: `Delete repository "${source.repo_name || source.repo_url} (${source.branch})"?`,
okText: 'Delete',
});
if (!ok) return;
try {
@ -381,7 +381,7 @@ export default function CodeSourcesPanel() {
setInFlight((prev) => prev.filter((x) => x.code_graph_id !== cgId));
if (selectedCodeAsset?.cgId === cgId) setSelectedCodeAsset(null);
if (selectedCgId === cgId) setSubView('list');
tea.notify.success('已删除');
tea.notify.success('Deleted');
fetchSources();
} catch (e: any) { tea.notify.error(e); }
};
@ -447,14 +447,14 @@ export default function CodeSourcesPanel() {
<div className="_codedetail-header-left">
<CodeIcon size={18} />
<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 && statusLabel(selected.status)}
{selected?.last_sync_at && <Text theme="label">{new Date(selected.last_sync_at).toLocaleString()}</Text>}
</div>
<div className="_codedetail-header-actions">
<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>
</div>
</div>
@ -466,22 +466,22 @@ export default function CodeSourcesPanel() {
{/* 统计 */}
{selected?.stats && (
<div className="_codedetail-stats">
<MetricsBoard title="文件" value={selected.stats.files?.toLocaleString() ?? '-'} />
<MetricsBoard title="图节点" value={selected.stats.nodes?.toLocaleString() ?? '-'} />
<MetricsBoard title="图边" value={selected.stats.edges?.toLocaleString() ?? '-'} />
<MetricsBoard title="Files" value={selected.stats.files?.toLocaleString() ?? '-'} />
<MetricsBoard title="Graph nodes" value={selected.stats.nodes?.toLocaleString() ?? '-'} />
<MetricsBoard title="Graph edges" value={selected.stats.edges?.toLocaleString() ?? '-'} />
</div>
)}
{/* 仓库信息 */}
{selected && (
<Card>
<Card.Body title="仓库信息">
<Card.Body title="Repository info">
<div className="_codedetail-info-grid">
<Text theme="label">Code Graph ID</Text>
<Text className="_codedetail-mono">{selected.code_graph_id}</Text>
<Text theme="label">Git 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>
</div>
</Card.Body>
@ -490,9 +490,9 @@ export default function CodeSourcesPanel() {
{/* 代码搜索 */}
<Card>
<Card.Body title="代码搜索">
<Card.Body title="Code search">
<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>
<div className="_codedetail-search-row">
<SearchBox
@ -500,7 +500,7 @@ export default function CodeSourcesPanel() {
value={searchQuery}
onChange={(v) => setSearchQuery(v)}
onSearch={() => void handleSearch()}
placeholder="输入符号名(函数 / 类 / 变量),返回其所在位置…"
placeholder="Enter a symbol name (function/class/variable) to find its location..."
/>
</div>
{searching && <StatusTip status="loading" />}
@ -514,9 +514,9 @@ export default function CodeSourcesPanel() {
{/* 代码探索 */}
<Card>
<Card.Body title="代码探索">
<Card.Body title="Code exploration">
<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>
<div className="_codedetail-search-row">
<SearchBox
@ -524,7 +524,7 @@ export default function CodeSourcesPanel() {
value={exploreQuery}
onChange={(v) => setExploreQuery(v)}
onSearch={() => void handleExplore()}
placeholder="用自然语言或符号名描述要理解的功能 / 流程,返回相关文件原文…"
placeholder="Describe the feature or flow you want to understand, in natural language or a symbol name..."
/>
</div>
{exploring && <StatusTip status="loading" />}
@ -558,18 +558,18 @@ export default function CodeSourcesPanel() {
value={agentFilter}
onChange={setAgentFilter}
disabled={teamAgents.length === 0}
placeholder="无可选 Agent"
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name}${agent.id}` }))}
placeholder="No agent available"
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name} (${agent.id})` }))}
/>
) : undefined}
subtitle={activeTeam ? `${activeTeam.name} · ${stats.total} 个仓库` : `${stats.total} 个仓库`}
subtitle={activeTeam ? `${activeTeam.name} · ${stats.total} repositories total` : `${stats.total} repositories total`}
actions={scopeTab !== 'fixed' ? (
<Button
onClick={() => setAllocateTarget(selectedCodeAsset)}
disabled={!selectedCodeAsset}
tooltip={!selectedCodeAsset ? '请先选中一条代码资产' : undefined}
tooltip={!selectedCodeAsset ? 'Select a code asset first' : undefined}
>
Agent
Assign to agent
</Button>
) : undefined}
/>
@ -577,29 +577,29 @@ export default function CodeSourcesPanel() {
<Card className="_asset-code-content-card">
<Card.Body>
<div className="_asset-code-stats">
<MetricsBoard title="仓库总数" value={stats.total} />
<MetricsBoard title="已就绪" value={stats.ready} />
<MetricsBoard title="处理中" value={stats.processing} />
<MetricsBoard title="文件总数" value={stats.totalFiles} />
<MetricsBoard title="Total repositories" value={stats.total} />
<MetricsBoard title="Ready" value={stats.ready} />
<MetricsBoard title="Processing" value={stats.processing} />
<MetricsBoard title="Total files" value={stats.totalFiles} />
</div>
<Table.ActionPanel>
<Justify
left={<Button type="primary" onClick={() => setShowRegister(true)}>+ </Button>}
left={<Button type="primary" onClick={() => setShowRegister(true)}>+ Register repository</Button>}
right={(
<div className="_asset-code-toolbar">
<SearchBox
value={keyword}
onChange={setKeyword}
placeholder="搜索名称 / 分支 / ID"
placeholder="Search name / branch / ID"
/>
<Segment
value={statusFilter}
onChange={(value) => setStatusFilter(value as StatusFilter)}
options={[
{ value: 'all', text: '全部状态' },
{ value: 'ready', text: '就绪' },
{ value: 'processing', text: '处理中' },
{ value: 'error', text: '异常' },
{ value: 'all', text: 'All statuses' },
{ value: 'ready', text: 'Ready' },
{ value: 'processing', text: 'Processing' },
{ value: 'error', text: 'Error' },
]}
/>
<Segment
@ -623,13 +623,13 @@ export default function CodeSourcesPanel() {
emptyText={(
<div className="_asset-code-empty">
<CodeIcon size="large" />
<Text></Text>
<Text theme="label">+ </Text>
<Text>No registered repositories</Text>
<Text theme="label">Click "+ Register repository" above to register your first one</Text>
</div>
)}
/>
) : filteredSources.length === 0 ? (
<StatusTip status="empty" emptyText="没有匹配的仓库,试试调整搜索或筛选条件。" />
<StatusTip status="empty" emptyText="No matching repositories. Try adjusting your search or filters." />
) : viewMode === 'card' ? (
<div className="_codelist-grid">
{filteredSources.map((source) => {
@ -645,7 +645,7 @@ export default function CodeSourcesPanel() {
type="button"
className="_codelist-card-head _codelist-card-name-trigger"
onClick={(event) => { event.stopPropagation(); openDetail(source.code_graph_id); }}
title={`查看 ${repoLabel} 详情`}
title={`View ${repoLabel} details`}
>
<CodeIcon size={16} />
<span className="_codelist-card-name">{repoLabel}</span>
@ -653,7 +653,7 @@ export default function CodeSourcesPanel() {
</button>
<div className="_codelist-card-meta">
{statusLabel(source.status)}
<span> {source.branch}</span>
<span>Branch {source.branch}</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>}
<span>{formatShortTime(source.last_sync_at)}</span>
@ -661,26 +661,26 @@ export default function CodeSourcesPanel() {
<div className="_codelist-card-owner">
<UsergroupIcon size={12} />
{scopeTab === 'fixed' ? (
`固定资产 · ${agentFilter || '未选择 Agent'}`
`Fixed asset · ${agentFilter || 'No agent selected'}`
) : source.owner_user_id ? (
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
) : (
'团队 Code 池'
'Team code pool'
)}
</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()}>
{scopeTab === 'fixed' ? (
<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 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} />
</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} />
</Button>
</div>
@ -696,14 +696,14 @@ export default function CodeSourcesPanel() {
columns={[
{
key: 'repo_name',
header: '仓库',
header: 'Repository',
width: 250,
render: (source) => (
<button
type="button"
className="_codelist-row-name"
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} />
<span>{source.repo_name || source.repo_url}</span>
@ -713,13 +713,13 @@ export default function CodeSourcesPanel() {
},
{
key: 'status',
header: '状态',
header: 'Status',
width: 120,
render: (source) => statusLabel(source.status),
},
{
key: 'branch',
header: '分支 / Commit',
header: 'Branch / commit',
width: 190,
render: (source) => (
<span className="_codelist-branch">
@ -730,25 +730,25 @@ export default function CodeSourcesPanel() {
},
{
key: 'stats',
header: '图谱统计',
header: 'Graph stats',
width: 150,
render: (source) => source.stats ? `${source.stats.nodes.toLocaleString()} nodes · ${source.stats.files.toLocaleString()} files` : '—',
},
{
key: 'owner',
header: '归属',
header: 'Owner',
width: 180,
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 ? (
<CodeOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
) : (
<Text theme="label"></Text>
<Text theme="label">Team pool</Text>
),
},
{
key: 'last_sync_at',
header: '最后更新时间',
header: 'Last updated',
width: 140,
render: (source) => <Text theme="label">{formatShortTime(source.last_sync_at)}</Text>,
},
@ -760,7 +760,7 @@ export default function CodeSourcesPanel() {
},
{
key: 'actions',
header: '操作',
header: 'Actions',
width: 280,
fixed: 'right',
render: (source) => {
@ -768,12 +768,12 @@ export default function CodeSourcesPanel() {
return (
<div className="_codelist-table-actions">
{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={() => handleDelete(source.code_graph_id)} className="_codelist-delete-action"></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">Delete</Button>
</div>
);
},
@ -792,10 +792,10 @@ export default function CodeSourcesPanel() {
// 已输入内容、非 SSH、但又不是合法 http(s) 地址 → 提示格式错误。
const showUrlError = !!trimmedRepo && !isSsh && !validUrl;
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>
<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
size="full"
value={formRepo}
@ -804,21 +804,21 @@ export default function CodeSourcesPanel() {
/>
</Form.Item>
{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 && (
<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" />
</Form.Item>
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={handleRegister} disabled={submitting || !formBranch.trim() || !validUrl} loading={submitting}>
{submitting ? '注册中…' : '注册'}
{submitting ? 'Registering…' : 'Register'}
</Button>
<Button onClick={() => setShowRegister(false)} disabled={submitting}></Button>
<Button onClick={() => setShowRegister(false)} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);
@ -833,9 +833,9 @@ export default function CodeSourcesPanel() {
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
onClose={() => setAllocateTarget(null)}
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);
tea.notify.success('已分配到 Agent');
tea.notify.success('Assigned to agent');
await fetchSources();
if (scopeTab === 'fixed') await fetchFixedBindings();
}}

View File

@ -48,37 +48,37 @@ export function AllocateMemoryDialog({
// 文案分支:不同来源说不同的话,避免"团队池里"这种错误措辞出现在 personal tab。
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 (
<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>
<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 ? (
<Alert type="warning">
Agent
<br />· Agent Agent
<br />· Agent Agent Agent
<br />· Agent
No agents available to allocate to. Possible reasons:
<br />· You haven't created any agents yet create one in "Agent management" first.
<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 />· This memory block is already bound to all of your agents, so no further allocation is needed.
</Alert>
) : (
<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 }))} />
</Form.Item>
)}
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={() => void submit()} disabled={!agentId || submitting} loading={submitting}></Button>
<Button onClick={onClose} disabled={submitting}></Button>
<Button type="primary" onClick={() => void submit()} disabled={!agentId || submitting} loading={submitting}>Allocate</Button>
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -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">
{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}>
<AppIcon size={12} /> {agentLabel(block.agent_id)}
<AppIcon size={12} /> Fixed to {agentLabel(block.agent_id)}
</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}>
<UsergroupIcon size={12} />
<UsergroupIcon size={12} /> Team memory pool
</span>
)}
{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>{new Date(block.updated_at_ms).toLocaleString()}</span>
<span>Updated: {new Date(block.updated_at_ms).toLocaleString()}</span>
</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-[11px] font-mono ${active ? '' : 'text-muted-foreground'}`}
title={known ? undefined : '点击加载该层内容'}
title={known ? undefined : 'Click to load this layer\'s content'}
>
{known ? cnt : '·'}
</span>
@ -175,7 +175,7 @@ export function BlockDetail({
})}
</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
@ -188,7 +188,7 @@ export function BlockDetail({
{showPager && (
<div className="mt-3 flex items-center justify-between gap-2 border-t border-border pt-3 text-[11px] text-muted-foreground">
<span>
{safePage + 1} / {pageCount} · {block.layers[layer].length} / {total}
Page {safePage + 1} / {pageCount} · showing {block.layers[layer].length} of {total} items
</span>
<div className="flex items-center gap-1">
<button
@ -196,14 +196,14 @@ export function BlockDetail({
disabled={layerLoading || safePage <= 0}
onClick={() => onLayerPageChange(safePage - 1)}
>
Previous
</button>
<button
className="rounded border px-2 py-1 disabled:cursor-not-allowed disabled:opacity-40"
disabled={layerLoading || safePage >= pageCount - 1}
onClick={() => onLayerPageChange(safePage + 1)}
>
Next
</button>
</div>
</div>
@ -228,7 +228,7 @@ function AtomicList({
if (items.length === 0) {
return (
<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>
);
}
@ -262,7 +262,7 @@ function AtomicList({
onClick={() => onLoadItem?.(it.id)}
disabled={loading}
>
{loading ? '加载中…' : hasBody ? '收起原文' : '展开原文'}
{loading ? 'Loading...' : hasBody ? 'Collapse raw text' : 'Expand raw text'}
</button>
</div>
) : (
@ -274,7 +274,7 @@ function AtomicList({
<ReactMarkdown remarkPlugins={[remarkGfm]}>{it.body}</ReactMarkdown>
</div>
) : 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>

View File

@ -125,7 +125,7 @@ export default function ChatMemoryPanel(
setBlocks(mapped);
} catch (e: any) {
if (seq !== fetchSeqRef.current) return;
tea.notify.error(e?.message || '加载记忆块失败');
tea.notify.error(e?.message || 'Failed to load memory blocks');
setBlocks([]);
} finally {
if (seq === fetchSeqRef.current) setBlocksLoading(false);
@ -253,7 +253,7 @@ export default function ChatMemoryPanel(
);
})
.catch((e: any) => {
if (!cancelled) tea.notify.error(e?.message || '加载层数据失败');
if (!cancelled) tea.notify.error(e?.message || 'Failed to load layer data');
})
.finally(() => {
if (!cancelled) setLayerLoading(false);
@ -314,7 +314,7 @@ export default function ChatMemoryPanel(
}),
);
} catch (e: any) {
tea.notify.error(e?.message || '加载 L2 原文失败');
tea.notify.error(e?.message || 'Failed to load L2 raw text');
} finally {
setLayerItemLoadingId(null);
}
@ -371,9 +371,9 @@ export default function ChatMemoryPanel(
// ── 操作 ──
async function handleDeleteBlock(id: string) {
const ok = await tea.confirm({
message: '确认解绑该记忆块?',
description: '将从当前 agent 移除该记忆块绑定。',
okText: '解绑',
message: 'Unbind this memory block?',
description: 'This removes the memory block binding from the current agent.',
okText: 'Unbind',
});
if (!ok) return;
try {
@ -382,9 +382,9 @@ export default function ChatMemoryPanel(
await chatMemoryApi.unbind(activeTeamId, id, block.agent_id);
setBlocks((prev) => prev.filter((b) => b.id !== id));
if (selectedId === id) setSelectedId(null);
tea.notify.success('已解绑');
tea.notify.success('Unbound');
} 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 {
if (!activeTeamId || !agent_id) {
tea.notify.warning('请先选择一个 Agent');
tea.notify.warning('Select an agent first');
return;
}
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);
fetchBlocks();
} 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故也无需精确数字
if (newScope === 'private') {
const ok = await tea.confirm({
message: '设为私密后,其他 Agent 将不能再使用这条记忆',
description: '如需再次共享,随时可以改回团队可见。',
okText: '设为私密',
message: 'Once set to private, other agents will no longer be able to use this memory',
description: 'You can switch it back to team-visible again at any time.',
okText: 'Set to private',
});
if (!ok) return;
}
try {
await chatMemoryApi.patchScope(block.id, newScope);
tea.notify.success(newScope === 'team' ? '已切换为团队可见' : '已切换为私密');
tea.notify.success(newScope === 'team' ? 'Switched to team-visible' : 'Switched to private');
fetchBlocks();
} 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 (
<div className="_asset-memory-page">
<AssetPageHeader
title="Chat_Memory · 原子记忆块"
title="Chat_Memory · Atomic memory blocks"
subtitle={
activeTeam
? `${activeTeam.name} · ${blocks.length} 条记忆`
: `${blocks.length} 条记忆`
? `${activeTeam.name} · ${blocks.length} memories`
: `${blocks.length} memories`
}
scope={
<Segment
@ -458,10 +458,10 @@ export default function ChatMemoryPanel(
value={agentFilter}
onChange={setAgentFilter}
disabled={ownedTeamAgents.length === 0}
placeholder="无可选 Agent"
placeholder="No agent available"
options={ownedTeamAgents.map((agent) => ({
value: agent.agent_id,
text: `${agent.name}${agent.agent_id}`,
text: `${agent.name} (${agent.agent_id})`,
}))}
/>
) : undefined
@ -475,18 +475,18 @@ export default function ChatMemoryPanel(
selected.uploaded_by_user_id !== currentUserId;
const disabled = !selected || isPrivateAndNotOwner;
const tooltip = !selected
? '请先选中一条记忆块'
? 'Select a memory block first'
: isPrivateAndNotOwner
? '该记忆已被 owner 设为私密,无法再分配给其他 Agent'
? 'This memory has been set to private by its owner and can no longer be allocated to other agents'
: undefined;
return (
<Button onClick={() => setShowAllocate(true)} disabled={disabled} tooltip={tooltip}>
Agent
Allocate to agent
</Button>
);
})()}
<Button type="primary" onClick={() => setShowImport(true)}>
Import memory
</Button>
</>
}
@ -507,14 +507,14 @@ export default function ChatMemoryPanel(
<section className="_asset-memory-list-column">
<div className="_asset-memory-list-panel">
<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">
{filtered.length} / {blocks.length}
</div>
</div>
{filtered.length === 0 ? (
<div className="text-[12px] text-muted-foreground px-3 py-4">
No matching memory blocks.
</div>
) : (
<ul className="space-y-0.5">
@ -541,7 +541,7 @@ export default function ChatMemoryPanel(
].join(' ')}
title={
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
}
>
@ -563,7 +563,7 @@ export default function ChatMemoryPanel(
color: 'var(--tea-color-text-warning-default)',
}}
>
owner
Set to private by owner
</span>
)}
</div>
@ -576,7 +576,7 @@ export default function ChatMemoryPanel(
borderColor: 'var(--tea-color-border-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)}
</span>
@ -588,20 +588,20 @@ export default function ChatMemoryPanel(
borderColor: 'var(--tea-color-border-warning-default)',
color: 'var(--tea-color-text-warning-default)',
}}
title="团队记忆池"
title="Team memory pool"
>
<UsergroupIcon size={12} />
<UsergroupIcon size={12} /> Team pool
</span>
)}
</div>
{b.uploaded_by_user_id && (
<div className="mt-1 text-[10px] text-muted-foreground">
Uploaded by:
<span className="font-mono text-foreground/70">
@{b.uploaded_by_user_id}
</span>
{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>
)}
@ -627,7 +627,7 @@ export default function ChatMemoryPanel(
e.stopPropagation();
handleDeleteBlock(b.id);
}}
title="解除该 Agent 对这条记忆块的固定绑定(记忆本身保留)"
title="Remove this agent's fixed binding to this memory block (the memory itself is kept)"
style={{
position: 'absolute',
right: '8px',
@ -644,7 +644,7 @@ export default function ChatMemoryPanel(
boxShadow: 'var(--tea-shadow-xs)',
}}
>
Unbind
</button>
)}
</li>
@ -660,7 +660,7 @@ export default function ChatMemoryPanel(
<div className="_asset-memory-detail-panel">
{!selected ? (
<div className="text-[12px] text-muted-foreground px-2 py-6">
Select a memory block on the left to see details.
</div>
) : (
<BlockDetail
@ -701,11 +701,11 @@ export default function ChatMemoryPanel(
onAllocated={async (agentId) => {
try {
await chatMemoryApi.allocate(activeTeamId!, selected.id, agentId);
tea.notify.success('已分配到 Agent');
tea.notify.success('Allocated to agent');
setShowAllocate(false);
fetchBlocks();
} catch (e: any) {
tea.notify.error(e?.message || '分配失败');
tea.notify.error(e?.message || 'Allocation failed');
}
}}
/>

View File

@ -11,8 +11,8 @@ interface ImportMessage {
const MAX_MESSAGES = 100;
const SAMPLE_JSON = `[
{ "role": "user", "content": "帮我 review 一下这段代码" },
{ "role": "assistant", "content": "好的,请贴出来。" }
{ "role": "user", "content": "Can you review this bit of code for me?" },
{ "role": "assistant", "content": "Sure, go ahead and paste it." }
]`;
type ParseResult =
@ -21,26 +21,26 @@ type ParseResult =
function parseMessages(text: string): ParseResult {
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;
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 (parsed.length === 0) return { ok: false, error: '消息数组不能为空。' };
if (parsed.length > MAX_MESSAGES) return { ok: false, error: `单次最多导入 ${MAX_MESSAGES} 条消息,当前 ${parsed.length} 条。` };
if (!Array.isArray(parsed)) return { ok: false, error: 'The root node must be a JSON array.' };
if (parsed.length === 0) return { ok: false, error: 'The messages array cannot be empty.' };
if (parsed.length > MAX_MESSAGES) return { ok: false, error: `A single import supports up to ${MAX_MESSAGES} messages; got ${parsed.length}.` };
const messages: ImportMessage[] = [];
for (let i = 0; i < parsed.length; i++) {
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;
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;
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 });
}
@ -95,17 +95,17 @@ export function ImportBlockDialog({
}
return (
<Modal visible caption="导入记忆" size="l" onClose={onClose} disableEscape={submitting}>
<Modal visible caption="Import memory" size="l" onClose={onClose} disableEscape={submitting}>
<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 */}
<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 ? (
<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}
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>
@ -113,11 +113,11 @@ export function ImportBlockDialog({
{/* 格式说明 */}
<Alert type="info" style={{ marginTop: 12 }}>
<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">
<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]">content</code></li>
<li> <strong>{MAX_MESSAGES}</strong> </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>: message body (non-empty string)</li>
<li>Up to <strong>{MAX_MESSAGES}</strong> messages per import</li>
</ul>
</div>
</Alert>
@ -125,12 +125,12 @@ export function ImportBlockDialog({
{/* 导入方式切换 */}
<div style={{ marginTop: 12 }}>
<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>
<Form layout="vertical" style={{ width: '100%', marginTop: 12 }}>
{importMode === 'paste' ? (
<Form.Item label="MessagesJSON 数组)">
<Form.Item label="Messages (JSON array)">
<Input.TextArea
size="full"
value={sessionPayload}
@ -142,13 +142,13 @@ export function ImportBlockDialog({
/>
</Form.Item>
) : (
<Form.Item label="选择 JSON 文件">
<Upload accept=".json,.txt,.md" beforeUpload={handleFilePicked}><Button></Button></Upload>
{fileName && <Text theme="text" parent="div" style={{ marginTop: 6 }}><Text parent="code">{fileName}</Text></Text>}
<Form.Item label="Select JSON file">
<Upload accept=".json,.txt,.md" beforeUpload={handleFilePicked}><Button>Choose file</Button></Upload>
{fileName && <Text theme="text" parent="div" style={{ marginTop: 6 }}>Selected: <Text parent="code">{fileName}</Text></Text>}
{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">
{sessionPayload.slice(0, 2000)}{sessionPayload.length > 2000 ? '\n…(已截断)' : ''}
{sessionPayload.slice(0, 2000)}{sessionPayload.length > 2000 ? '\n…(truncated)' : ''}
</pre>
</Form.Item>
)}
@ -159,7 +159,7 @@ export function ImportBlockDialog({
{/* 解析结果反馈 */}
{sessionPayload.trim() && parsed.ok && (
<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>
)}
{sessionPayload.trim() && !parsed.ok && (
@ -172,11 +172,11 @@ export function ImportBlockDialog({
disabled={!canSubmit}
loading={submitting}
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 onClick={onClose} disabled={submitting}></Button>
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -22,9 +22,9 @@ function MemoryOwnerTag({ userId, isCurrentUser }: { userId: string; isCurrentUs
const displayName = useUserDisplayName(userId);
return (
<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}
{isCurrentUser && '(你)'}
{isCurrentUser && ' (you)'}
</span>
</Tag>
);
@ -53,20 +53,20 @@ export function PersonalAssetsTable({
<Card.Body>
{/* 顶部 */}
<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">
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>
</div>
{loading ? (
<div className="_cm-personal-empty">
<Text theme="weak"></Text>
<Text theme="weak">Loading...</Text>
</div>
) : blocks.length === 0 ? (
<div className="_cm-personal-empty">
<Text theme="weak" parent="div">
· Agent
No memory assets yet · creating an agent automatically generates a private memory for it
</Text>
</div>
) : (
@ -88,7 +88,7 @@ export function PersonalAssetsTable({
<div className="_cm-personal-asset-main">
<div className="_cm-personal-asset-name" title={block.title}>{block.title}</div>
<div className="_cm-personal-asset-meta">
{new Date(block.updated_at_ms).toLocaleString()}
Updated: {new Date(block.updated_at_ms).toLocaleString()}
</div>
<div className="_cm-personal-asset-id" title={block.id}>{block.id}</div>
{block.uploaded_by_user_id && (
@ -96,11 +96,11 @@ export function PersonalAssetsTable({
<MemoryOwnerTag userId={block.uploaded_by_user_id} isCurrentUser={ownerIsMe} />
{isTeam ? (
<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 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>
)}
</div>
@ -114,16 +114,16 @@ export function PersonalAssetsTable({
<Button
type={isTeam ? 'primary' : 'weak'}
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
type={isPrivate ? 'primary' : 'weak'}
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>
</div>
</div>

View File

@ -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';
export const LAYERS: LayerMeta[] = [
{ id: 'L0', label: 'L0 · 对话原文', short: '对话原文', desc: '原始对话 / 工具调用流水,不做压缩', tone: 'default' },
{ id: 'L1', label: 'L1 · 原子记忆', short: '原子记忆', desc: '从原文抽取出来的最小事实 / 约束', tone: 'brand' },
{ id: 'L2', label: 'L2 · 场景记忆', short: '场景记忆', desc: '围绕场景聚合的多条原子记忆总结', tone: 'success' },
{ id: 'L3', label: 'L3 · 核心记忆', short: '核心记忆', desc: '沉淀的核心准则 / 模板 / 决策', tone: 'warning' },
{ id: 'L0', label: 'L0 · Raw conversation', short: 'Raw conversation', desc: 'Raw conversation / tool-call log, uncompressed', tone: 'default' },
{ id: 'L1', label: 'L1 · Atomic memory', short: 'Atomic memory', desc: 'Minimal facts / constraints extracted from the raw text', tone: 'brand' },
{ id: 'L2', label: 'L2 · Scene memory', short: 'Scene memory', desc: 'Summary aggregating multiple atomic memories around a scene', tone: 'success' },
{ 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> = {
all: '全部',
team: '团队资产',
fixed: 'Agent 资产',
scope: '可分配资产',
personal: '我的资产分配',
all: 'All',
team: 'Team assets',
fixed: 'Agent assets',
scope: 'Allocatable assets',
personal: 'My asset allocations',
};

View File

@ -27,7 +27,7 @@ export function formatShortTime(ms: number): string {
yesterday.getFullYear() === d.getFullYear() &&
yesterday.getMonth() === d.getMonth() &&
yesterday.getDate() === d.getDate()
) return '昨天';
) return 'Yesterday';
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

View File

@ -64,7 +64,7 @@ export default function ForkSkillDialog(props: {
async function submit(): Promise<void> {
if (!agentId) {
setError('请选择 agent。');
setError('Select an agent.');
return;
}
setError(null);
@ -86,7 +86,7 @@ export default function ForkSkillDialog(props: {
});
if (existing.items.some((s) => s.name === newName)) {
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
? `(已复制 ${resources.length} 个资源文件)`
? ` (copied ${resources.length} resource files)`
: (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);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
@ -141,27 +141,27 @@ export default function ForkSkillDialog(props: {
}
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>
<Form>
<Form.Item label="说明">
<Form.Text> {props.skillName} agent skill agent </Form.Text>
<Form.Item label="Description">
<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 label="Agent" required>
<Select
size="full"
value={agentId}
onChange={setAgentId}
placeholder="请选择 agent"
placeholder="Select an agent"
options={props.agents.map((a) => ({ value: a.id, text: `${a.id} · ${a.name}` }))}
/>
</Form.Item>
<Form.Item label="副本名" required>
<Form.Item label="Copy name" required>
<Input
size="full"
value={newName}
onChange={setNewName}
placeholder="副本 skill 名称"
placeholder="Name for the copy"
/>
</Form.Item>
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
@ -170,7 +170,7 @@ export default function ForkSkillDialog(props: {
</Modal.Body>
<Modal.Footer>
<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>
);

View File

@ -101,7 +101,7 @@ function partitionFiles(files: File[]): {
skillName: null,
mainFile: null,
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('/');
@ -159,30 +159,30 @@ export default function ImportSkillDialog(props: {
try {
const agentId = props.target === 'fixed' ? (selectedAgentId || props.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 ====
if (mode === 'session') {
const raw = sessionPayload.trim();
if (!raw) throw new Error('请粘贴对话 JSON。');
if (!raw) throw new Error('Paste the conversation JSON.');
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(raw);
} 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)) {
throw new Error('对话 JSON 缺少 messages 数组字段。');
throw new Error('The conversation JSON is missing the messages array field.');
}
const msgs = (parsed as { messages: unknown[] }).messages;
if (msgs.length === 0) {
throw new Error('对话 messages 不能为空。');
throw new Error('Conversation messages cannot be empty.');
}
// 后端 extract 接口限制 messages 最多 500 条(见 iWiki §3.13
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
@ -238,18 +238,18 @@ export default function ImportSkillDialog(props: {
if (raced === 'timeout') {
softTimedOut = true;
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) {
setResult(
'提交成功,提取任务已受理。预计 1-3 分钟后完成,请稍后刷新 skill 列表查看结果。'
+ `\n任务 ID${raced.task_id}`,
'Submitted successfully — the extraction task has been accepted. It should finish in 1-3 minutes; refresh the skill list later to see the result.'
+ `\nTask ID: ${raced.task_id}`,
);
} else {
// extractPromise 被软超时后 catch 成 null 的分支(正常不会走到这里,
// 因为软超时已经先设过 result 了),兜底保持一致文案。
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);
@ -258,14 +258,14 @@ export default function ImportSkillDialog(props: {
// ==== 目录导入:走 v3 create + files/write ====
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 fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const nameMatch = fmMatch?.[1].match(/^name:\s*(.+)$/m);
const name = nameMatch?.[1].trim().replace(/^["']|["']$/g, '') || partition.skillName || '';
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(
({ path, file }) => ({ path, file, isBinary: !looksLikeText(file) }),
@ -297,7 +297,7 @@ export default function ImportSkillDialog(props: {
resources: resources.length ? resources : undefined,
});
setResult(`导入成功:${name}${resourceFiles.length} 个资源文件)`);
setResult(`Imported successfully: ${name} (${resourceFiles.length} resource files)`);
setTimeout(() => props.onImported(), 800);
} catch (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;
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>
<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
ChatMemoryPanel.ImportBlockDialog 便 agentId
*/}
{showAgentPicker && (
<Form layout="vertical" style={{ width: '100%' }}>
<Form.Item
label="归属 Agent必选"
extra="Skill 将作为该 agent 的固定资产 —— 仅当 task 关联了该 agent 时才会被加载。"
label="Owning agent (required)"
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 ? (
<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={selectedAgentId}
onChange={setSelectedAgentId}
placeholder="-- 选择 Agent --"
options={props.agents!.map((a) => ({ value: a.id, text: `${a.name}${a.id}` }))}
placeholder="-- Select agent --"
options={props.agents!.map((a) => ({ value: a.id, text: `${a.name} (${a.id})` }))}
/>
)}
</Form.Item>
@ -345,14 +345,14 @@ export default function ImportSkillDialog(props: {
value={mode}
onChange={(v) => setMode(v as Mode)}
options={[
{ value: 'directory', text: '目录导入' },
{ value: 'session', text: '对话导入' },
{ value: 'directory', text: 'Directory import' },
{ value: 'session', text: 'Conversation import' },
]}
/>
{mode === 'directory' && (
<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 Button webkitdirectory/directory 广
@ -371,24 +371,24 @@ export default function ImportSkillDialog(props: {
{...({ webkitdirectory: '', directory: '' } as Record<string, string>)}
/>
<Button onClick={() => fileInputRef.current?.click()}>
<FolderOpenIcon size={14} />
<FolderOpenIcon size={14} /> Choose folder
</Button>
{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 && (
<div className="_memory-isd-partition-box">
<div>
Main file:
{partition.mainFile ? (
<span className="_memory-isd-main-file">
{partition.mainFile.webkitRelativePath || partition.mainFile.name}
</span>
) : (
<span className="_memory-isd-error-text"> SKILL.md</span>
<span className="_memory-isd-error-text">SKILL.md not found</span>
)}
</div>
<div>{partition.resources.length} </div>
<div>Resource files: {partition.resources.length}</div>
{partition.resources.length > 0 && (
<ul className="_memory-isd-resource-list">
{partition.resources.map((r) => (
@ -407,11 +407,11 @@ export default function ImportSkillDialog(props: {
{mode === 'session' && (
<div className="_memory-isd-section">
<Alert type="info">
agent skill agent
JSON <span className="_memory-isd-mono-inline">messages</span>
Paste a conversation with this agent the system will automatically distill a reusable skill for it.
The conversation JSON must include a <span className="_memory-isd-mono-inline">messages</span> array.
</Alert>
<Form layout="vertical" style={{ width: '100%' }}>
<Form.Item label="对话 JSON">
<Form.Item label="Conversation JSON">
<Input.TextArea
size="full"
value={sessionPayload}
@ -422,11 +422,11 @@ export default function ImportSkillDialog(props: {
session_id: 'demo-user-extract-demo-1',
task_id: 'default',
messages: [
{ role: 'user', content: '我们的 PostgreSQL 14 主库今天又卡死了…' },
{ role: 'assistant', content: '先 ssh 到主库节点,查看慢查询日志…' },
{ role: 'tool_call', content: '调用 bash 执行: tail -100 /var/log/postgresql/slow.log' },
{ role: 'user', content: 'Our PostgreSQL 14 primary froze again today…' },
{ role: 'assistant', content: 'Let me SSH into the primary node first and check the slow query 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: 'assistant', content: '发现一条慢查询耗时 120s建议添加索引…' },
{ role: 'assistant', content: 'Found a slow query taking 120s — recommend adding an index…' },
],
},
null,
@ -451,12 +451,12 @@ export default function ImportSkillDialog(props: {
|| (mode === 'directory' ? !partition?.mainFile : !sessionPayload.trim())
|| (props.target === 'fixed' && !selectedAgentId)
}
title={props.target === 'fixed' && !selectedAgentId ? '请先选择归属 agent' : ''}
title={props.target === 'fixed' && !selectedAgentId ? 'Select an owning agent first' : ''}
loading={submitting}
>
{mode === 'session' ? '开始提取' : '导入 Skill'}
{mode === 'session' ? 'Start extraction' : 'Import skill'}
</Button>
<Button onClick={props.onClose} disabled={submitting}></Button>
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -134,7 +134,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
} catch (err) {
setFilePreview({
path,
content: `读取失败:${err instanceof Error ? err.message : String(err)}`,
content: `Failed to read: ${err instanceof Error ? err.message : String(err)}`,
encoding: 'utf-8',
size_bytes: 0,
mime_type: 'text/plain',
@ -149,7 +149,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
return (
<Card className="_memory-skill-detail-card">
<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>
);
@ -168,7 +168,7 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
{!stale && error && (
<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 && (
<>
{/* Metadata */}
@ -204,10 +204,10 @@ export default function SkillDetailPane(props: { skillName: string | null; skill
{/* Files */}
<div className="_memory-skill-detail-section">
<Text theme="label" parent="div" className="_memory-skill-detail-section-title">
({currentView.manifest?.length ?? 0})
Attached resources ({currentView.manifest?.length ?? 0})
</Text>
{!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">
<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.Body>
{filePreviewLoading ? (
<Text theme="weak" parent="div"></Text>
<Text theme="weak" parent="div">Loading...</Text>
) : filePreview.encoding === 'base64' ? (
<Text theme="weak" parent="div">
({filePreview.size_bytes} bytes)base64
Binary file ({filePreview.size_bytes} bytes) base64 content omitted.
</Text>
) : (
<pre className="_memory-skill-file-content">{filePreview.content}</pre>

View File

@ -46,20 +46,20 @@ import './skills-list.css';
type Tab = 'team' | 'fixed' | 'personal';
const TAB_LABELS: Record<Tab, string> = {
team: '团队资产',
fixed: 'Agent 资产',
personal: '我的资产分配',
team: 'Team assets',
fixed: 'Agent assets',
personal: 'My asset allocations',
};
/** 统一 Skill 列表的用户归属徽章:优先展示 display_name再回退 user_id。 */
function SkillOwnerTag({ userId, isCurrentUser }: { userId: string; isCurrentUser: boolean }) {
const displayName = useUserDisplayName(userId);
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">
<span className="_memory-skill-tag-content">
<UserIcon size={10} /> {displayName || userId}
{isCurrentUser && '(你)'}
{isCurrentUser && ' (you)'}
</span>
</Tag>
</span>
@ -110,7 +110,7 @@ export default function SkillsPanel({
.catch((err) => {
if (cancelled) return;
// agent 加载失败不致命(列表 fallback 显示 agent_id但仍给出提示。
tea.notify.error(err?.message || '加载 Agent 信息失败');
tea.notify.error(err?.message || 'Failed to load agent info');
setAgentNameMap({});
setTeamAgents([]);
});
@ -345,7 +345,7 @@ export default function SkillsPanel({
if (selectedSkillId === skill.skill_id) {
setSelectedSkillId(null);
}
tea.notify.success(`已删除 Skill「${skill.name}`);
tea.notify.success(`Deleted skill "${skill.name}"`);
void refresh();
} catch (err) {
tea.notify.error(err);
@ -362,10 +362,10 @@ export default function SkillsPanel({
<div className="_memory-skills-body">
{/* 固定资产的 Agent 选择器与 Code 页 "Agent 资产" 选项栏保持相同呈现。 */}
<AssetPageHeader
title="Skill 资产管理"
title="Skill asset management"
subtitle={activeTeam
? `${activeTeam.name} · ${tab === 'personal' ? '我的' : ''}${skills.length} 个 Skill`
: `${tab === 'personal' ? '我的' : ''}${skills.length} 个 Skill`}
? `${activeTeam.name} · ${tab === 'personal' ? 'My ' : ''}${skills.length} skills`
: `${tab === 'personal' ? 'My ' : ''}${skills.length} skills`}
scope={(
<Segment
value={tab}
@ -380,8 +380,8 @@ export default function SkillsPanel({
value={selectedAgent}
onChange={(value) => { setSelectedAgent(value); setSelectedSkillId(null); }}
disabled={teamAgents.length === 0}
placeholder="无可选 Agent"
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name}${agent.id}` }))}
placeholder="No agent available"
options={teamAgents.map((agent) => ({ value: agent.id, text: `${agent.name} (${agent.id})` }))}
/>
) : undefined}
actions={(
@ -391,21 +391,21 @@ export default function SkillsPanel({
const forkableInPersonal = tab === 'personal' && !!selectedPersonalAsset;
const canFork = forkableInTeam || forkableInPersonal;
const tooltip = tab === 'fixed'
? '请在「团队」或「我的资产分配」视图选中一条 skill'
? 'Select a skill in the "Team" or "My asset allocations" view'
: tab === 'team'
? (!selectedSkillId ? '请先选中一条 skill' : undefined)
: (!selectedPersonalAsset ? '请先选中一条 skill' : undefined);
? (!selectedSkillId ? 'Select a skill first' : undefined)
: (!selectedPersonalAsset ? 'Select a skill first' : undefined);
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
type="primary"
onClick={() => setShowImport(true)}
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>
</>
)}
@ -453,7 +453,7 @@ export default function SkillsPanel({
)}
</Text>
{/* loading 时不显示条数 —— 旧数据已清空,显示"0 条"会误导 */}
{!loading && <Text theme="weak">{skillsWithCache.length} </Text>}
{!loading && <Text theme="weak">{skillsWithCache.length} items</Text>}
</div>
{loading ? (
<div className="_memory-skills-list-items">
@ -469,10 +469,10 @@ export default function SkillsPanel({
<div className="_memory-skills-list-empty">
<Text theme="weak">
{tab === 'fixed' && !selectedAgent
? '请选择一个 agent。'
? 'Select an agent.'
: tab === 'fixed'
? `Agent "${selectedAgent}" 暂无固定 skill。点击右上「导入 Skill」直接导入或在「团队」视图选中一条 skill 后通过「Fork」分发。`
: '团队里还没有任何"共享"skill。skill 新建时默认私密,只有 owner 自己能看到;如需让整个团队看到,需要 owner 在「我的资产分配」tab 里点共享按钮。'}
? `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".`
: '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>
</div>
) : (
@ -500,13 +500,13 @@ export default function SkillsPanel({
{/* 可见性徽章:从 visibilityMap 里读;用 Tea Tag 渲染(与 TaskWorkbench 一致) */}
{vis === 'private' && (
<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>
)}
{/* 团队 tab 的全部条目都已是共享资产,不重复占用标题行空间。 */}
{vis === 'team' && tab !== 'team' && (
<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>
)}
{vis && vis !== 'private' && vis !== 'team' && (
@ -514,7 +514,7 @@ export default function SkillsPanel({
)}
{/* owner agent 徽章:自己 owner 的高亮warning别人的用 default */}
{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
theme={ownerIsMe ? 'warning' : 'default'}
variant="soft"
@ -535,7 +535,7 @@ export default function SkillsPanel({
<Button
type="icon"
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"
onClick={async (e: any) => {
e?.stopPropagation();
@ -544,13 +544,13 @@ export default function SkillsPanel({
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent
// 其他 Agent 下同名的独立副本不受影响。按"彻底删除"语义描述。
const ok = await tea.confirm({
message: `确认彻底删除 Skill「${s.name}」?`,
message: `Permanently delete skill "${s.name}"?`,
description:
'删除后该 Skill 将从所属 Agent 卸载,且不可恢复。' +
'其他 Agent 下同名的独立副本不受影响。' +
'如仅需临时停用,请考虑将其设为"私密"而非删除。',
okText: '彻底删除',
cancelText: '取消',
'Deleting it will uninstall the skill from its agent and cannot be undone. ' +
'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: 'Delete permanently',
cancelText: 'Cancel',
});
if (ok) {
void handleDelete(s);
@ -755,13 +755,13 @@ function PersonalAssetTab({
// Skill 数据面为软归档meta asset 经钩子物理删除。前端按"彻底删除"描述。
// Skill 按 owner_agent_id 独立 —— 删除只影响其所属 Agent其他 Agent 的独立副本不受影响。
const ok = await tea.confirm({
message: `确认彻底删除 Skill「${asset.name}」?`,
message: `Permanently delete skill "${asset.name}"?`,
description:
'删除后该 Skill 将从所属 Agent 卸载,且不可恢复。' +
'其他 Agent 下同名的独立副本不受影响。' +
'如仅需临时停用,请考虑将其设为"私密"而非删除。',
okText: '彻底删除',
cancelText: '取消',
'Deleting it will uninstall the skill from its agent and cannot be undone. ' +
'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: 'Delete permanently',
cancelText: 'Cancel',
});
if (!ok) return;
setBusyId(asset.asset_id);
@ -800,7 +800,7 @@ function PersonalAssetTab({
}
setAssets((prev) => prev.filter((a) => a.asset_id !== asset.asset_id));
if (selectedAssetId === asset.asset_id) onSelectAsset?.(null);
tea.notify.success(`已删除 Skill「${asset.name}`);
tea.notify.success(`Deleted skill "${asset.name}"`);
} catch (err) {
tea.notify.error(err);
} finally {
@ -813,20 +813,20 @@ function PersonalAssetTab({
<Card.Body>
{/* 顶部 */}
<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">
owner · / team
Shows only assets you own · toggle "Shared / Private" to control whether other team members can see it
</Text>
</div>
{loading ? (
<div className="_memory-personal-empty">
<Text theme="weak"></Text>
<Text theme="weak">Loading...</Text>
</div>
) : assets.length === 0 ? (
<div className="_memory-personal-empty">
<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>
</div>
) : (
@ -859,16 +859,16 @@ function PersonalAssetTab({
<div className="_memory-personal-asset-badges">
{cachedAgentId && (
<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}
</span>
</Tag>
)}
{asset.owner_user_id && (
<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}
{asset.owner_user_id === currentUser && '(你)'}
{asset.owner_user_id === currentUser && ' (you)'}
</span>
</Tag>
)}
@ -888,24 +888,24 @@ function PersonalAssetTab({
type={isTeam ? 'primary' : 'weak'}
disabled={isBusy}
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
type={isPrivate ? 'primary' : 'weak'}
disabled={isBusy}
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>
</div>
{/* 删除Tea 图标按钮error 主题),二次确认在 handleDelete 内 */}
<Button type="text"
disabled={isBusy}
tooltip="彻底删除该 Skill不可恢复"
tooltip="Permanently delete this skill (cannot be undone)"
className="_memory-personal-asset-delete"
onClick={(e: any) => {
e?.stopPropagation();

View File

@ -113,9 +113,9 @@ export default function ApiKeyPanel() {
async function handleDelete(key: UserKey) {
const ok = await tea.confirm({
message: `确认吊销 Key「${key.key_prefix || key.key_id}」?`,
description: '吊销后对应客户端将立即失效,且不可恢复。',
okText: '吊销',
message: `Revoke key "${key.key_prefix || key.key_id}"?`,
description: 'Once revoked, the corresponding client will lose access immediately, and this cannot be undone.',
okText: 'Revoke',
});
if (!ok) return;
try {
@ -140,8 +140,8 @@ export default function ApiKeyPanel() {
<Alert type="success" onClose={() => setFreshKey(null)}>
<div className="_memory-apikey-fresh">
<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>
<div className="_memory-apikey-fresh-code-row">
<code className="_memory-apikey-fresh-code">{freshKey.secret}</code>
@ -161,9 +161,9 @@ export default function ApiKeyPanel() {
<Justify
left={
<div>
<H3>User_Key </H3>
<H3>User_Key management</H3>
<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>
</div>
}
@ -176,7 +176,7 @@ export default function ApiKeyPanel() {
}}
>
<AddIcon size={14} />
Key
Create key
</Button>
) : null
}
@ -209,26 +209,26 @@ export default function ApiKeyPanel() {
},
{
key: 'created_at',
header: '创建时间',
header: 'Created at',
width: 180,
render: (key) => <Text theme="text">{formatTime(key.created_at)}</Text>,
},
{
key: 'expires_at',
header: '失效时间',
header: 'Expires at',
width: 180,
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 ? (
<Text theme="text">{formatTime(key.expires_at)}</Text>
) : (
<Text theme="weak"></Text>
<Text theme="weak">Never expires</Text>
);
},
},
{
key: 'actions',
header: '操作',
header: 'Actions',
width: 100,
align: 'right',
render: (key) => (
@ -236,7 +236,7 @@ export default function ApiKeyPanel() {
disabled={!!key.revoked_at}
onClick={() => void handleDelete(key)}
>
Revoke
</Button>
),
},
@ -246,9 +246,9 @@ export default function ApiKeyPanel() {
isLoading: loading,
emptyText: (
<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">
Key Key
Click "Create key" in the top-right to create your first key
</div>
</div>
),
@ -265,10 +265,10 @@ export default function ApiKeyPanel() {
LoginGate fallback
*/}
<Card>
<Card.Body title="客户端接入地址">
<Card.Body title="Client access address">
{auth?.instance_name && (
<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>
</div>
)}
@ -278,7 +278,7 @@ export default function ApiKeyPanel() {
if (!gatewayEndpoint) {
return (
<Text theme="weak" style={{ fontSize: 11 }}>
Loading access address
</Text>
);
}
@ -308,7 +308,7 @@ export default function ApiKeyPanel() {
{ep.url}
</code>
<Copy text={ep.url}>
<Button></Button>
<Button>Copy</Button>
</Copy>
</div>
</div>
@ -319,22 +319,22 @@ export default function ApiKeyPanel() {
</Card>
{/* ===== 新建弹窗:只需设置「过期时间」(可留空=永不过期),不再需要名称 ===== */}
{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>
<Form>
<Form.Item label="过期时间" extra="留空表示永不过期">
<Form.Item label="Expiration" extra="Leave blank for never expires">
<DatePicker
value={newExpiresAt ?? undefined}
onChange={(v) => setNewExpiresAt(v)}
disabledDate={(d) => !d.isBefore(moment().startOf('day'))}
placeholder="留空表示永不过期"
placeholder="Leave blank for never expires"
/>
</Form.Item>
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={() => void handleCreate()} disabled={creating} loading={creating}></Button>
<Button onClick={() => setShowCreate(false)} disabled={creating}></Button>
<Button type="primary" onClick={() => void handleCreate()} disabled={creating} loading={creating}>Create</Button>
<Button onClick={() => setShowCreate(false)} disabled={creating}>Cancel</Button>
</Modal.Footer>
</Modal>
)}

View File

@ -133,7 +133,7 @@ export default function AgentEditDialog({
const nextRolePrompt = rolePrompt.trim();
const nextRulesPrompt = rulesPrompt.trim();
if (!nextName) {
tea.notify.error('Agent 名称不能为空。');
tea.notify.error('Agent name cannot be empty.');
return;
}
setSavingPrompt(true);
@ -149,10 +149,10 @@ export default function AgentEditDialog({
}),
});
invalidateBackendCache();
tea.notify.success('Agent 信息已保存。');
tea.notify.success('Agent information saved.');
onClose();
} 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 {
setSavingPrompt(false);
}
@ -199,7 +199,7 @@ export default function AgentEditDialog({
const msg = err instanceof Error ? err.message : String(err);
// 只读模式下后端可能拒绝访问非自己的 agent 资产NOT_YOUR_AGENT这是预期行为。
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]);
return (
<Modal visible caption="Agent 详情" size="l" onClose={onClose}>
<Modal visible caption="Agent details" size="l" onClose={onClose}>
<Modal.Body>
<div className="_memory-form-stack">
<div className="_memory-modal-description">{agent.agent_id}</div>
<LightField label="名称">
<LightField label="Name">
<Input size="full" value={name} onChange={setName} disabled={savingPrompt} />
</LightField>
<LightField label="一句话描述">
<LightField label="One-line description">
<Input.TextArea size="full" value={description} onChange={setDescription} rows={2} disabled={savingPrompt} />
</LightField>
<LightField label="角色定位 prompt">
<LightField label="Role prompt">
<Input.TextArea
size="full"
value={rolePrompt}
onChange={setRolePrompt}
rows={3}
disabled={savingPrompt}
placeholder="描述这个 agent 扮演什么角色 / 职责定位..."
placeholder="Describe what role/responsibility this agent plays..."
/>
</LightField>
<LightField label="规则固定 prompt">
<LightField label="Rules prompt">
<Input.TextArea
size="full"
value={rulesPrompt}
@ -238,23 +238,23 @@ export default function AgentEditDialog({
rows={4}
disabled={savingPrompt}
className="_memory-mono-textarea"
placeholder="为 Agent 设定行为规则提示词..."
placeholder="Set behavior rule prompts for the agent..."
/>
</LightField>
<div className="_memory-asset-section">
{assets.loading ? (
<div className="_memory-asset-loading"></div>
<div className="_memory-asset-loading">Loading team assets</div>
) : (
<>
<div className="_memory-asset-toolbar">
<span className="_memory-asset-toolbar-label"></span>
<span className="_memory-asset-toolbar-hint"> · </span>
<span className="_memory-asset-toolbar-label">Atomic capabilities</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 className="_memory-collapse-group-stack">
<CollapseGroup
icon={<BooksIcon size={16} />}
title="Wiki 知识库"
title="Wiki knowledge base"
selectedCount={llmWikis.length}
totalCount={boundWikis.length}
open={wikiOpen}
@ -286,7 +286,7 @@ export default function AgentEditDialog({
</CollapseGroup>
<CollapseGroup
icon={<ToolsIcon size={16} />}
title="Skill 技能"
title="Skill"
selectedCount={skills.length}
totalCount={boundSkills.length}
open={skillsOpen}
@ -323,9 +323,9 @@ export default function AgentEditDialog({
</div>
</Modal.Body>
<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}>
Save changes
</Button>
</Modal.Footer>
</Modal>

View File

@ -95,7 +95,7 @@ export default function AgentGrid({
className={`_memory-agents-name-trigger${editable ? ' _memory-agents-name-trigger--editable' : ''}`}
onClick={() => editable && onEditAgent(agent)}
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-name" title={agent.name}>{agent.name}</span>
@ -108,7 +108,7 @@ export default function AgentGrid({
const ownerIsMe = agent.owner_user_id === currentUser;
return (
<Tag theme={ownerIsMe ? 'warning' : 'default'} size="sm">
{agent.owner_user_id || '未设置'}{ownerIsMe && '(你)'}
{agent.owner_user_id || 'not set'}{ownerIsMe && ' (you)'}
</Tag>
);
}
@ -131,9 +131,9 @@ export default function AgentGrid({
<div>
<h2 className="_memory-agents-section-title">Agents</h2>
<div className="_memory-agents-section-subtitle">
team{activeTeam.name}
<span className="_memory-mono-inline">{activeTeam.team_id}</span>
Agent · {agentsLoading ? '加载中…' : `${agents.length}`}
Agents you created in team "{activeTeam.name}"
<span className="_memory-mono-inline"> ({activeTeam.team_id})</span>
· {agentsLoading ? 'Loading…' : `${agents.length} total`}
</div>
</div>
</div>
@ -145,9 +145,9 @@ export default function AgentGrid({
type="primary"
onClick={onCreateAgent}
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>
}
right={
@ -155,7 +155,7 @@ export default function AgentGrid({
<SearchBox
value={keyword}
onChange={setKeyword}
placeholder="搜索 Agent 名称 / 描述 / ID"
placeholder="Search by agent name / description / ID"
/>
{canSeeAllAgents && (
<Select
@ -163,7 +163,7 @@ export default function AgentGrid({
onChange={setOwnerFilter}
appearance="button"
options={[
{ value: '', text: '全部 Owner' },
{ value: '', text: 'All owners' },
...ownerOptions.map((ownerId) => ({ value: ownerId, text: ownerId })),
]}
matchButtonWidth
@ -183,16 +183,16 @@ export default function AgentGrid({
</Table.ActionPanel>
{agentsLoading && agents.length === 0 ? (
<div className="_memory-agents-empty"> Agent</div>
<div className="_memory-agents-empty">Loading agents</div>
) : filteredAgents.length === 0 ? (
<div className="_memory-agents-empty">
{agents.length === 0
? isAdmin
? '当前 team 下还没有 Agent'
: '还没有 Agent · 点击左上角「+ 新建 Agent」创建第一个'
? 'No agents in the current team yet'
: 'No agents yet · click "+ Create agent" in the top-left to create your first one'
: canSeeAllAgents
? '没有符合搜索或 Owner 筛选条件的 Agent'
: '没有符合搜索条件的 Agent'}
? 'No agents match the search or owner filter'
: 'No agents match the search'}
</div>
) : viewMode === 'card' ? (
<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 className="_memory-agents-card-head">{renderName(agent)}</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">
<span>owner</span>
{renderOwner(agent)}
{!editable && <span className="_memory-agents-readonly">· </span>}
{!editable && <span className="_memory-agents-readonly">· read-only</span>}
</div>
{renderAssets(agent)}
<div className="_memory-agents-card-actions">
@ -214,9 +214,9 @@ export default function AgentGrid({
type="text"
disabled={!editable}
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>
</div>
</div>
@ -231,7 +231,7 @@ export default function AgentGrid({
columns={[
{
key: 'name',
header: '名称',
header: 'Name',
width: 240,
render: (agent: StoreAgent) => renderName(agent, true),
},
@ -243,7 +243,7 @@ export default function AgentGrid({
},
{
key: 'assets',
header: '挂载资产',
header: 'Mounted assets',
render: (agent: StoreAgent) => {
const counts = mountedCounts[agent.agent_id] ?? emptyMountedCounts();
return (
@ -255,19 +255,19 @@ export default function AgentGrid({
},
{
key: 'description',
header: '描述',
render: (agent: StoreAgent) => <span className="_memory-agents-list-description">{agent.description || '暂无描述'}</span>,
header: 'Description',
render: (agent: StoreAgent) => <span className="_memory-agents-list-description">{agent.description || 'No description'}</span>,
},
{
key: 'actions',
header: '操作',
header: 'Actions',
width: 90,
fixed: 'right',
render: (agent: StoreAgent) => {
const editable = canEdit(agent);
return (
<Button type="link" disabled={!editable} onClick={() => onDeleteAgent(agent)}>
Delete
</Button>
);
},

View File

@ -128,31 +128,31 @@ export default function CreateAgentDialog({
}
return (
<Modal visible caption="创建 Agent" size="l" onClose={onClose} disableEscape={busy}>
<Modal visible caption="Create agent" size="l" onClose={onClose} disableEscape={busy}>
<Modal.Body>
<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">
<span className="_memory-target-team-avatar">{team.name.slice(0, 1).toUpperCase()}</span>
<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">
<span className="_memory-target-team-name">{team.name}</span>
<Tag size="sm">{team.team_id}</Tag>
</div>
</div>
<div className="_memory-target-team-hint">
team
To switch team, go to
<br />
the top-left corner
</div>
</div>
<div className="_memory-template-box">
<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">
· / prompt /
Optional · one click prefills description / prompts / atomic capabilities, name still required
</span>
</div>
<div className="_memory-template-chip-row">
@ -175,9 +175,9 @@ export default function CreateAgentDialog({
<button
type="button"
onClick={() => handleDeleteTemplate(tpl)}
title="删除该自定义模板"
title="Delete this custom template"
className="_memory-template-chip-close"
aria-label="删除该自定义模板"
aria-label="Delete this custom template"
>
<CloseIcon size={10} />
</button>
@ -190,102 +190,102 @@ export default function CreateAgentDialog({
onClick={openSaveTemplateForm}
className="_memory-template-save-btn"
>
<AddIcon size={11} />
<AddIcon size={11} /> Save as template
</button>
</div>
{saveTplOpen && (
<div className="_memory-template-save-form">
<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>
<Input
size="full"
value={tplName}
onChange={setTplName}
placeholder="模板名(必填),如:安全审计 Reviewer"
placeholder="Template name (required), e.g. Security audit reviewer"
/>
<Input
size="full"
value={tplSummary}
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
size="full"
value={tplDescription}
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
size="full"
rows={3}
value={tplRolePrompt}
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
size="full"
rows={4}
value={tplRulesPrompt}
onChange={setTplRulesPrompt}
placeholder={'rules prompt · 硬约束,建议编号列表(选填)\n1. …\n2. …'}
placeholder={'rules prompt · hard constraints, numbered list recommended (optional)\n1. …\n2. …'}
className="_memory-mono-textarea"
/>
<div className="_memory-template-save-actions">
<Button onClick={resetSaveTemplateForm}></Button>
<Button onClick={resetSaveTemplateForm}>Cancel</Button>
<Button type="primary"
disabled={!tplName.trim()}
onClick={handleSaveTemplate}
>
Save template
</Button>
</div>
</div>
)}
</div>
<LightField label="名字 *">
<LightField label="Name *">
<Input
autoFocus
size="full"
value={name}
onChange={setName}
placeholder=" Code Reviewer"
placeholder="e.g. Code Reviewer"
/>
<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>
</LightField>
<LightField label="一句话描述" hint="选填 · 留空也可以,详情页随时改。">
<LightField label="One-line description" hint="Optional · can be left blank, editable anytime from the detail page.">
<Input
size="full"
value={description}
onChange={setDescription}
placeholder="一句话功能介绍:这个 agent 是干什么的?(选填)"
placeholder="One-line description: what does this agent do? (optional)"
/>
</LightField>
<LightField
label="角色定位 prompt"
hint="role prompt · 选填 · 描述这个 agent 扮演什么角色 / 职责定位,创建后可在详情页补。"
label="Role prompt"
hint="role prompt · optional · describe what role/responsibility this agent plays, can be filled in later on the detail page."
>
<Input.TextArea
size="full"
value={rolePrompt}
onChange={setRolePrompt}
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
label="规则固定 prompt"
hint="rules prompt · 选填 · 注入到每次对话开头的硬约束,建议用编号列表,创建后可在详情页补。"
label="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
size="full"
@ -298,11 +298,11 @@ export default function CreateAgentDialog({
</LightField>
{assets.loading ? (
<div className="_memory-asset-loading"></div>
<div className="_memory-asset-loading">Loading team assets</div>
) : (
<>
<div className="_memory-asset-toolbar">
<span className="_memory-asset-toolbar-label"></span>
<span className="_memory-asset-toolbar-label">Atomic capabilities:</span>
<button
type="button"
onClick={() => {
@ -313,7 +313,7 @@ export default function CreateAgentDialog({
}}
className="_memory-asset-toolbar-btn"
>
Select all
</button>
{totalSelected > 0 && (
<button
@ -326,13 +326,13 @@ export default function CreateAgentDialog({
}}
className="_memory-asset-toolbar-btn"
>
Clear
</button>
)}
</div>
<CollapseGroup
icon={<BooksIcon size={16} />}
title="Wiki 知识库"
title="Wiki knowledge base"
selectedCount={llmWikis.length}
totalCount={assets.wikis.length}
open={wikiOpen}
@ -360,7 +360,7 @@ export default function CreateAgentDialog({
</CollapseGroup>
<CollapseGroup
icon={<ToolsIcon size={16} />}
title="Skill 技能"
title="Skill"
selectedCount={skills.length}
totalCount={assets.skills.length}
open={skillsOpen}
@ -406,9 +406,9 @@ export default function CreateAgentDialog({
chatMemories,
})}
>
Create
</Button>
<Button onClick={onClose} disabled={busy}></Button>
<Button onClick={onClose} disabled={busy}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -18,32 +18,32 @@ export default function CreateTeamDialog({
const [description, setDescription] = useState('');
const canSubmit = name.trim().length > 0 && !busy;
return (
<Modal visible caption="创建 Team" size="s" onClose={onClose} disableEscape={busy}>
<Modal visible caption="Create team" size="s" onClose={onClose} disableEscape={busy}>
<Modal.Body>
<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
autoFocus
size="full"
value={name}
onChange={setName}
placeholder="例如 tdai-memory · 后端组"
placeholder="e.g. godcall-memory · backend team"
/>
</Form.Item>
<Form.Item label="描述">
<Form.Item label="Description">
<Input.TextArea
size="full"
value={description}
onChange={setDescription}
rows={3}
placeholder="一句话说明 team 范围与目标"
placeholder="One sentence describing the team's scope and goal"
/>
</Form.Item>
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" disabled={!canSubmit} loading={busy} onClick={() => onCreate({ name: name.trim(), description: description.trim() })}></Button>
<Button onClick={onClose} disabled={busy}></Button>
<Button type="primary" disabled={!canSubmit} loading={busy} onClick={() => onCreate({ name: name.trim(), description: description.trim() })}>Create</Button>
<Button onClick={onClose} disabled={busy}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -30,9 +30,9 @@ export function MemberSection({
async function handleRemove(userId: string) {
const ok = await tea.confirm({
message: `移除成员 ${userId}`,
description: '此操作仅将该用户移出当前团队,不会删除用户账号。',
okText: '移除',
message: `Remove member ${userId}?`,
description: 'This only removes the user from the current team; it does not delete the user account.',
okText: 'Remove',
});
if (!ok) return;
setRemoving(userId);
@ -51,17 +51,17 @@ export function MemberSection({
<div className="_memory-section-header">
<div className="_memory-section-header-info">
<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>
</div>
<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>
{canAddMember && (
<Button onClick={onAdd} title="按 user_id 邀请成员加入">
<AddIcon size={14} />
<Button onClick={onAdd} title="Invite a member by user_id">
<AddIcon size={14} /> Add member
</Button>
)}
</div>
@ -118,7 +118,7 @@ function MemberCard({
<div className="_memory-member-info">
<div className="_memory-member-id">
{displayName}
{isMe && <span className="_memory-member-me-tag"> </span>}
{isMe && <span className="_memory-member-me-tag"> (you)</span>}
</div>
{hasUsername && (
<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">
{role}
{isOwner ? ' · 创建者' : ''}
{isOwner ? ' · Creator' : ''}
</div>
</div>
<div className="_memory-member-actions">
@ -137,8 +137,8 @@ function MemberCard({
onClick={(e) => { e.stopPropagation(); onRemove(); }}
disabled={removing}
className="_memory-member-remove-btn"
title="移除该成员"
aria-label="移除该成员"
title="Remove this member"
aria-label="Remove this member"
>
{removing ? '…' : <CloseIcon size={12} />}
</button>
@ -188,11 +188,11 @@ export function AddMemberDialog({
async function submitExisting() {
const id = userId.trim();
if (!id) {
setError('请输入对方的 user_id。');
setError('Enter the user_id of the person to add.');
return;
}
if (id === currentUser) {
setError('不能添加自己;如需调整角色,请由其他 team admin 操作。');
setError('You cannot add yourself; ask another team admin to adjust roles.');
return;
}
setSubmitting(true);
@ -211,12 +211,12 @@ export function AddMemberDialog({
async function submitNew() {
const username = newUsername.trim();
if (!username) {
setError('请输入用户名。');
setError('Enter a username.');
return;
}
// 用户名只允许英文字母、数字、下划线(与后端 user_id 段校验规则一致)
if (!/^[A-Za-z0-9_]+$/.test(username)) {
setError('用户名仅支持英文字母、数字、下划线,不能包含其他符号或空格。');
setError('Usernames may only contain letters, numbers, and underscores — no other symbols or spaces.');
return;
}
setSubmitting(true);
@ -260,15 +260,15 @@ export function AddMemberDialog({
return (
<Modal
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"
onClose={onClose}
disableEscape={submitting}
>
<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.Item label="方式">
<Form.Item label="Method">
{canCreateUser ? (
<Segment
value={mode}
@ -278,13 +278,13 @@ export function AddMemberDialog({
if (v === 'new') setRole('member');
}}
options={[
{ value: 'existing', text: '添加已有用户' },
{ value: 'new', text: '新建用户并加入团队' },
{ value: 'existing', text: 'Add existing user' },
{ value: 'new', text: 'Create user and add to team' },
]}
/>
) : (
<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>
)}
</Form.Item>
@ -301,14 +301,14 @@ export function AddMemberDialog({
setError(null);
}}
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>
</Form.Item>
) : (
<>
<Form.Item label="用户名" required>
<Form.Item label="Username" required>
<div>
<Input
autoFocus
@ -319,39 +319,39 @@ export function AddMemberDialog({
setError(null);
}}
onPressEnter={() => void handleSubmit()}
placeholder="例如 alice"
placeholder="e.g. alice"
/>
{newUsername.trim() && !/^[A-Za-z0-9_]+$/.test(newUsername.trim()) ? (
<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 className="_memory-field-hint">线</div>
<div className="_memory-field-hint">Letters, numbers, underscores; cannot be changed after creation</div>
)}
</div>
</Form.Item>
</>
)}
<Form.Item label="角色">
<Form.Item label="Role">
<Select
size="full"
value="member"
disabled
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>
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={() => void handleSubmit()} disabled={!canSubmit || submitting} loading={submitting}>
{mode === 'existing' ? '添加' : '新建并添加'}
{mode === 'existing' ? 'Add' : 'Create and add'}
</Button>
<Button onClick={onClose} disabled={submitting}></Button>
<Button onClick={onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);
@ -370,16 +370,16 @@ export function CreatedUserKeyModal({
const [copied, setCopied] = useState(false);
return (
<Modal visible caption="用户创建成功" size="m" onClose={onClose}>
<Modal visible caption="User created successfully" size="m" onClose={onClose}>
<Modal.Body>
<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]">
{info.keyValue ? (
<>
<Alert type="warning">
<strong> User_Key </strong>
Key
<strong>The User_Key below is shown only this once</strong> copy it now and send it
securely to the user. Once this dialog is closed, the key cannot be viewed again.
</Alert>
<Form.Item label="User_Key">
<div className="flex items-center gap-2">
@ -388,7 +388,7 @@ export function CreatedUserKeyModal({
</code>
<Copy text={info.keyValue}>
<Button onClick={() => setCopied(true)}>
{copied ? '已复制' : '复制'}
{copied ? 'Copied' : 'Copy'}
</Button>
</Copy>
</div>
@ -396,8 +396,8 @@ export function CreatedUserKeyModal({
</>
) : (
<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
className="mt-1 block rounded px-2 py-1 text-[12px] font-mono select-all"
style={{ background: 'var(--tea-color-bg-primary-default)' }}
@ -410,7 +410,7 @@ export function CreatedUserKeyModal({
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={onClose}></Button>
<Button type="primary" onClick={onClose}>Got it</Button>
</Modal.Footer>
</Modal>
);

View File

@ -174,14 +174,14 @@ export default function TeamManagementPanel({
)
) {
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;
}
const ok = await tea.confirm({
message: `确认删除 Agent「${agent.name}」?`,
description: `${agent.agent_id} 删除后不可恢复。`,
okText: '删除',
message: `Delete agent "${agent.name}"?`,
description: `${agent.agent_id} cannot be recovered after deletion.`,
okText: 'Delete',
});
if (!ok) return;
try {
@ -193,7 +193,7 @@ export default function TeamManagementPanel({
const raw = err instanceof Error ? err.message : String(err);
if (raw.includes('SKILL_DELETE_FAILED')) {
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 {
tea.notify.error(errMsg(err));
@ -237,7 +237,7 @@ export default function TeamManagementPanel({
className="_memory-team-header-name"
style={{ color: 'var(--muted-foreground)' }}
>
Loading
</span>
</div>
</div>
@ -251,7 +251,7 @@ export default function TeamManagementPanel({
<div className="_memory-team-header-meta-row">
<span className="_memory-team-header-name">{activeTeam.name}</span>
<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>
{activeTeam.description && (
<div className="_memory-team-header-desc">{activeTeam.description}</div>
@ -260,24 +260,24 @@ export default function TeamManagementPanel({
</div>
) : (
<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 className="_memory-team-header-ops">
{!teamsLoading && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
<Button onClick={() => setShowCreateTeam(true)} title="创建一个新 team">
<AddIcon size={14} /> Team
<Button onClick={() => setShowCreateTeam(true)} title="Create a new team">
<AddIcon size={14} /> Create team
</Button>
)}
{activeTeam && (isTeamAdmin(activeTeam, currentUser) || _isAdmin) && (
<Button
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>
)}
</div>
@ -289,7 +289,7 @@ export default function TeamManagementPanel({
className="_memory-panel-card"
style={{ padding: '2rem', textAlign: 'center', color: 'var(--muted-foreground)' }}
>
Loading
</div>
) : !activeTeam ? (
<EmptyTeamState onCreateTeam={() => setShowCreateTeam(true)} />
@ -375,13 +375,13 @@ function EmptyTeamState({ onCreateTeam }: { onCreateTeam: () => void }) {
return (
<div className="_memory-empty-team">
<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">
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>
<Button type="primary" onClick={onCreateTeam} className="_memory-empty-team-cta">
<AddIcon size={14} /> Team
<AddIcon size={14} /> Create your first team
</Button>
</div>
);

View File

@ -70,7 +70,7 @@ export function CollapseGroup({
<span className="_memory-collapse-group-icon">{icon}</span>
<span className="_memory-collapse-group-title">{title}</span>
<span className="_memory-collapse-group-count">
{hideTotal ? `已绑定 ${selectedCount}` : `已选 ${selectedCount} / 共 ${totalCount}`}
{hideTotal ? `Bound ${selectedCount}` : `${selectedCount} selected / ${totalCount} total`}
</span>
</button>
{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-title">{a.title}</span>
<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>
</Checkbox>

View File

@ -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));
}, [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 (!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 (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">No graph data</span></div>;
const typeSet = new Set(filteredData.nodes.map((n) => n.type));
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>
<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"
placeholder="搜索节点..."
placeholder="Search nodes..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
@ -244,11 +244,11 @@ export default function KnowledgeGraph({ data, loading, onNodeClick, highlightNo
)}
</div>
<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 === "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 === "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")}>Community</button>
</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"}`}
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>
</div>

View File

@ -168,12 +168,12 @@ const WIKI_STATUS_BADGE: Record<
WikiDetail['status'],
{ label: string; theme: 'warning' | 'success' | 'error' | 'default' }
> = {
draft: { label: '待加工', theme: 'warning' },
pending: { label: '排队中', theme: 'warning' },
processing: { label: '加工中', theme: 'warning' },
ready: { label: '就绪', theme: 'success' },
failed: { label: '失败', theme: 'error' },
missing: { label: '已丢失', theme: 'error' },
draft: { label: 'Awaiting processing', theme: 'warning' },
pending: { label: 'Queued', theme: 'warning' },
processing: { label: 'Processing', theme: 'warning' },
ready: { label: 'Ready', theme: 'success' },
failed: { label: 'Failed', theme: 'error' },
missing: { label: 'Lost', theme: 'error' },
};
function WikiStatusBadge({ status }: { status: WikiDetail['status'] }) {
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';
const SCOPE_LABELS: Record<WikiScopeTab, string> = {
all: '全部',
team: '团队 Wiki 池',
fixed: 'Agent 资产',
scope: '可配置范围',
all: 'All',
team: 'Team wiki pool',
fixed: 'Agent assets',
scope: 'Configurable scope',
};
/**
@ -243,7 +243,7 @@ function WikiOwnerLabel({ userId, currentUserId }: { userId: string; currentUser
return (
<span title={`Owner: ${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>
);
}
@ -306,7 +306,7 @@ export default function WikiSourcesPanel() {
const items = await knowledgeApi.wiki.agentFixed(agentFilter);
setFixedBoundIds(new Set(items.map((it) => it.knowledge_id)));
} catch (e: any) {
tea.notify.error(e?.message || '加载固定资产失败');
tea.notify.error(e?.message || 'Failed to load fixed assets');
setFixedBoundIds(new Set());
}
}, [agentFilter]);
@ -480,7 +480,7 @@ export default function WikiSourcesPanel() {
} finally {
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(
@ -529,19 +529,19 @@ export default function WikiSourcesPanel() {
async function handleUnbindWiki(wikiId: string) {
if (!agentFilter) return;
const ok = await tea.confirm({
message: '确认解绑该 Wiki',
description: '将从当前 agent 移除该 Wiki 绑定。',
okText: '解绑',
message: 'Unbind this wiki?',
description: 'This removes the wiki binding from the current agent.',
okText: 'Unbind',
});
if (!ok) return;
try {
await knowledgeApi.wiki.unbind(wikiId, agentFilter);
tea.notify.success('已解绑');
tea.notify.success('Unbound');
if (selectedWikiId === wikiId) setSelectedWikiId('');
await fetchFixedBindings();
await fetchSources();
} 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);
try {
await knowledgeApi.wiki.create(activeTeamId, newName.trim());
tea.notify.success(`Wiki${newName.trim()}」已创建`);
tea.notify.success(`Wiki "${newName.trim()}" created`);
setShowCreate(false);
setNewName('');
fetchSources();
@ -566,7 +566,7 @@ export default function WikiSourcesPanel() {
// 防御:同一时间只允许一个 Wiki 提取,避免并发 ingest 导致后端排队混乱。
// 按钮已按 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;
}
const wiki = sources.find((s) => s.wiki_id === wikiId);
@ -576,7 +576,7 @@ export default function WikiSourcesPanel() {
wikiId,
wiki: name,
currentFile: '',
detail: '正在触发抽取...',
detail: 'Triggering extraction...',
done: 0,
total: 100,
checkCount: 0,
@ -592,14 +592,14 @@ export default function WikiSourcesPanel() {
const checkedAt = new Date(ev.ts).toLocaleTimeString();
if (ev.type === 'file_start') {
next.currentFile = ev.file || '';
next.detail = ev.detail || '处理中...';
next.detail = ev.detail || 'Processing...';
next.done = ev.done ?? prev.done;
next.total = ev.total ?? prev.total;
next.lastCheckedAt = checkedAt;
} else if (ev.type === 'file_done') {
next.done = ev.done ?? prev.done;
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.lastCheckedAt = checkedAt;
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') {
next.done = ev.done ?? 100;
next.total = ev.total ?? 100;
next.detail = ev.detail || '抽取完成';
next.detail = ev.detail || 'Extraction complete';
next.lastCheckedAt = checkedAt;
}
return next;
@ -624,30 +624,30 @@ export default function WikiSourcesPanel() {
active: false,
done: 100,
total: 100,
detail: `完成!当前 ${result.ingested}`,
detail: `Done! ${result.ingested} pages so far`,
currentFile: '',
}));
tea.notify.success(`Wiki 抽取完成,共 ${result.ingested}`);
tea.notify.success(`Wiki extraction complete — ${result.ingested} pages total`);
fetchSources();
fetchDetail(wikiId);
},
onError: (err) => {
setIngestState((prev) => ({ ...prev, active: false, detail: `错误: ${err}` }));
tea.notify.error(err || 'Wiki 抽取失败');
setIngestState((prev) => ({ ...prev, active: false, detail: `Error: ${err}` }));
tea.notify.error(err || 'Wiki extraction failed');
},
},
activeTeamId ?? '',
);
setIngestState((prev) =>
prev.active
? { ...prev, active: false, detail: prev.log.length > 0 ? '完成' : prev.detail }
? { ...prev, active: false, detail: prev.log.length > 0 ? 'Done' : prev.detail }
: prev,
);
fetchSources();
};
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;
try {
await knowledgeApi.wiki.delete(wikiId);
@ -689,7 +689,7 @@ export default function WikiSourcesPanel() {
setReadContent(r?.content || '');
} catch (e: any) {
setReadContent('');
tea.notify.error(e?.message || '读取页面内容失败');
tea.notify.error(e?.message || 'Failed to read page content');
} finally {
setReadLoading(false);
}
@ -699,42 +699,42 @@ export default function WikiSourcesPanel() {
if (!selectedWikiId) return;
const ref = (page as any).id || page.path;
const ok = await tea.confirm({
message: `确认删除页面「${page.title || ref}」?`,
description: '会删除该 wiki 页面并清理引用。',
okText: '删除',
message: `Delete page "${page.title || ref}"?`,
description: 'This deletes the wiki page and cleans up references.',
okText: 'Delete',
});
if (!ok) return;
try {
await knowledgeApi.wiki.pageDelete(selectedWikiId, [ref]);
tea.notify.success('已删除页面');
tea.notify.success('Page deleted');
if (selectedPage && ((selectedPage as any).id || selectedPage.path) === ref) {
setSelectedPage(null);
setReadContent('');
}
await fetchDetail(selectedWikiId);
} catch (e: any) {
tea.notify.error(e?.message || '删除页面失败');
tea.notify.error(e?.message || 'Failed to delete page');
}
};
const handleDeleteRaw = async (filename: string) => {
if (!selectedWikiId) return;
const ok = await tea.confirm({
message: `确认删除原始文档「${filename}」?`,
description: '会删除原始文档,并同步清理由它派生的页面。',
okText: '删除',
message: `Delete original document "${filename}"?`,
description: 'This deletes the original document and cleans up pages derived from it.',
okText: 'Delete',
});
if (!ok) return;
try {
await knowledgeApi.wiki.rawDelete(selectedWikiId, [filename]);
tea.notify.success('已删除原始文档');
tea.notify.success('Original document deleted');
if (selectedPage?.path === `raw/${filename}`) {
setSelectedPage(null);
setReadContent('');
}
await fetchDetail(selectedWikiId);
} 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;
return tea.confirm({
message: `检测到 ${existing.length} 个同名文件`,
description: `继续上传将覆盖原有内容:${formatOverwriteFilenames(existing)}`,
okText: '覆盖并上传',
cancelText: '取消',
message: `Found ${existing.length} file(s) with the same name`,
description: `Continuing will overwrite existing content: ${formatOverwriteFilenames(existing)}`,
okText: 'Overwrite and upload',
cancelText: 'Cancel',
});
} 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;
}
};
@ -778,15 +778,15 @@ export default function WikiSourcesPanel() {
*/
const offerIngestAfterUpload = async (wikiId: string, uploadedCount: number) => {
const shouldIngest = await tea.confirm({
message: `${uploadedCount} 个文档已上传`,
description: '文档尚未抽取为可检索页面。现在开始抽取后,才能在页面、图谱和搜索中使用这些内容。',
okText: '开始抽取',
cancelText: '稍后处理',
message: `${uploadedCount} document(s) uploaded`,
description: "Documents haven't been extracted into searchable pages yet. Start extraction to use this content in pages, the graph, and search.",
okText: 'Start extraction',
cancelText: 'Later',
});
if (shouldIngest) {
void handleIngest(wikiId);
} 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;
setSubmitting(false);
if (failures.length === 0) {
tea.notify.success(`已上传 ${valid.length} 个文档`);
tea.notify.success(`${valid.length} document(s) uploaded`);
setMdDocs([{ filename: '', content: '' }]);
setShowAddDoc(false);
fetchDetail(selectedWikiId);
@ -827,8 +827,8 @@ export default function WikiSourcesPanel() {
.slice(0, 3)
.map((f) => `${f.filename}: ${f.error}`)
.join('\n');
const more = failures.length > 3 ? `\n…及其它 ${failures.length - 3}` : '';
tea.notify.error(`${okCount} 个成功,${failures.length} 个失败:\n${shown}${more}`);
const more = failures.length > 3 ? `\n...and ${failures.length - 3} more` : '';
tea.notify.error(`${okCount} succeeded, ${failures.length} failed:\n${shown}${more}`);
fetchDetail(selectedWikiId);
setRawRefreshKey((k) => k + 1);
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 succeeded = results.length - failed;
if (failed === 0) {
tea.notify.success(`已上传 ${succeeded} 个文件`);
tea.notify.success(`${succeeded} file(s) uploaded`);
setPendingFiles([]);
setUploadProgress({});
setShowAddDoc(false);
@ -881,7 +881,7 @@ export default function WikiSourcesPanel() {
if (r.status === 'rejected')
setUploadProgress((prev) => ({ ...prev, [pendingFiles[i].name]: 'error' }));
});
tea.notify.error(`${succeeded} 个成功,${failed} 个失败`);
tea.notify.error(`${succeeded} succeeded, ${failed} failed`);
fetchDetail(selectedWikiId);
setRawRefreshKey((k) => k + 1);
if (succeeded > 0) await offerIngestAfterUpload(selectedWikiId, succeeded);
@ -925,13 +925,13 @@ export default function WikiSourcesPanel() {
if (hasManualIngestState || !runningWiki) return ingestState;
const stage = wikiStageLabel(runningWiki.status, runningWiki.internal_status);
const pageHint =
typeof runningWiki.page_count === 'number' ? `,当前 ${runningWiki.page_count}` : '';
typeof runningWiki.page_count === 'number' ? `, ${runningWiki.page_count} pages so far` : '';
return {
active: true,
wikiId: runningWiki.wiki_id ?? '',
wiki: runningWiki.name,
currentFile: '',
detail: `状态恢复:${stage}${pageHint}`,
detail: `Status restored: ${stage}${pageHint}`,
done: wikiProgressPercent(runningWiki.status, runningWiki.internal_status),
total: 100,
checkCount: 0,
@ -999,7 +999,7 @@ export default function WikiSourcesPanel() {
<Card.Body className="_wiki-detail-header-body">
<div className="_wiki-detail-breadcrumb">
<Button type="text" onClick={() => { fetchSources(); setSubView('list'); }}>
<ArrowLeftIcon size={12} /> Wiki
<ArrowLeftIcon size={12} /> Wiki knowledge base
</Button>
<span>/</span>
<span>{wikiName}</span>
@ -1009,7 +1009,7 @@ export default function WikiSourcesPanel() {
<BooksIcon size={18} />
<span className="_wiki-detail-title">{wikiName}</span>
{source && <WikiStatusBadge status={source.status} />}
<Text theme="label">{pages.length} </Text>
<Text theme="label">{pages.length} pages</Text>
</div>
<div className="_wiki-detail-header-actions">
<Button
@ -1019,7 +1019,7 @@ export default function WikiSourcesPanel() {
setAddDocTab('file');
}}
>
<AttachIcon size={14} />
<AttachIcon size={14} /> Add
</Button>
<Button
type="primary"
@ -1028,7 +1028,7 @@ export default function WikiSourcesPanel() {
loading={ingestBusy && displayIngestState.wiki === wikiName}
>
{ingestBusy && displayIngestState.wiki === wikiName ? (
'处理中'
'Processing'
) : (
<>
<StarIcon size={14} /> Ingest
@ -1052,14 +1052,14 @@ export default function WikiSourcesPanel() {
) : (
<CheckCircleIcon size={14} />
)}{' '}
Ingest{displayIngestState.wiki}
Ingest: {displayIngestState.wiki}
</Text>
{!displayIngestState.active && (
<Button
type="text"
onClick={() => setIngestState((state) => ({ ...state, log: [] }))}
>
Clear
</Button>
)}
</div>
@ -1078,9 +1078,9 @@ export default function WikiSourcesPanel() {
</div>
{displayIngestState.checkCount > 0 && (
<Text theme="label">
{displayIngestState.checkCount}
Checked {displayIngestState.checkCount} times
{displayIngestState.lastCheckedAt
? `,最近 ${displayIngestState.lastCheckedAt}`
? `, last at ${displayIngestState.lastCheckedAt}`
: ''}
</Text>
)}
@ -1124,7 +1124,7 @@ export default function WikiSourcesPanel() {
label: (
<span className="_wiki-detail-tab-label">
<ChartBarIcon size={14} />
Overview
</span>
),
},
@ -1133,7 +1133,7 @@ export default function WikiSourcesPanel() {
label: (
<span className="_wiki-detail-tab-label">
<ArchitectureIcon size={14} />
Graph
</span>
),
},
@ -1142,7 +1142,7 @@ export default function WikiSourcesPanel() {
label: (
<span className="_wiki-detail-tab-label">
<FileIcon size={14} />
Pages
</span>
),
},
@ -1151,7 +1151,7 @@ export default function WikiSourcesPanel() {
label: (
<span className="_wiki-detail-tab-label">
<SearchIcon size={14} />
Search
</span>
),
},
@ -1160,14 +1160,14 @@ export default function WikiSourcesPanel() {
<TabPanel id="overview">
<div className="_wiki-detail-overview">
<div className="_wiki-detail-overview-stats">
<MetricsBoard title="总页面数" value={pages.length} />
<MetricsBoard title="页面类型" value={types.length} />
<MetricsBoard title="页面间链接" value={edgeCount} />
<MetricsBoard title="Total pages" value={pages.length} />
<MetricsBoard title="Page types" value={types.length} />
<MetricsBoard title="Links between pages" value={edgeCount} />
</div>
<Card bordered>
<Card.Body title="类型分布">
<Card.Body title="Type distribution">
{types.length === 0 ? (
<StatusTip status="empty" emptyText="暂无页面数据" />
<StatusTip status="empty" emptyText="No page data" />
) : (
<div className="_wiki-detail-type-dist">
{types.map((type) => {
@ -1192,7 +1192,7 @@ export default function WikiSourcesPanel() {
/>
</span>
<Text theme="label" className="_wiki-detail-type-count">
{count}{pct}%
{count} ({pct}%)
</Text>
</div>
);
@ -1202,9 +1202,9 @@ export default function WikiSourcesPanel() {
</Card.Body>
</Card>
<Card bordered>
<Card.Body title="页面一览">
<Card.Body title="Page overview">
{pages.length === 0 ? (
<StatusTip status="empty" emptyText="暂无页面" />
<StatusTip status="empty" emptyText="No pages" />
) : (
<div className="_wiki-detail-overview-grid">
{pages.slice(0, 9).map((page) => (
@ -1278,7 +1278,7 @@ export default function WikiSourcesPanel() {
.then((result: any) => setReadContent(result?.items?.[0]?.content || ''))
.catch((error: any) => {
setReadContent('');
tea.notify.error(error?.message || '读取原始文档失败');
tea.notify.error(error?.message || 'Failed to read original document');
})
.finally(() => setReadLoading(false));
}}
@ -1290,12 +1290,12 @@ export default function WikiSourcesPanel() {
value={searchQuery}
onChange={setSearchQuery}
onSearch={handleSearch}
placeholder="搜索文档内容…"
placeholder="Search document content..."
/>
{searching && <StatusTip status="loading" />}
{!searching && searchResults.length > 0 && (
<>
<Text theme="label">{searchResults.length} </Text>
<Text theme="label">{searchResults.length} results</Text>
<div className="_wiki-detail-search-results">
{searchResults.map((result, index) => (
<button
@ -1336,7 +1336,7 @@ export default function WikiSourcesPanel() {
</>
)}
{!searching && searchResults.length === 0 && searchQuery && (
<StatusTip status="empty" emptyText="未找到匹配结果" />
<StatusTip status="empty" emptyText="No matching results found" />
)}
</div>
</TabPanel>
@ -1346,16 +1346,16 @@ export default function WikiSourcesPanel() {
{showAddDoc && (
<Modal
visible
caption={`添加文档到 ${wikiName}`}
caption={`Add document to ${wikiName}`}
size="m"
onClose={() => setShowAddDoc(false)}
disableEscape={submitting}
>
<Modal.Body>
<Alert type="info"></Alert>
<Alert type="info">Choose a way to import documents</Alert>
<Tabs
tabs={[
{ id: 'file', label: '上传文件' },
{ id: 'file', label: 'Upload file' },
{ id: 'markdown', label: 'Markdown' },
]}
activeId={addDocTab}
@ -1374,13 +1374,13 @@ export default function WikiSourcesPanel() {
const rejected = all.length - allowed.length;
if (rejected > 0) {
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]);
}}
>
<Text theme="weak"> Markdown </Text>
<Text theme="weak">Drag and drop or click to select Markdown files (multiple allowed)</Text>
</div>
{pendingFiles.length > 0 && (
<div className="_wiki-detail-upload-files">
@ -1410,7 +1410,7 @@ export default function WikiSourcesPanel() {
setPendingFiles((prev) => prev.filter((_, j) => j !== i))
}
>
Delete
</Button>
)}
</div>
@ -1419,14 +1419,14 @@ export default function WikiSourcesPanel() {
)}
{pendingFiles.length > 0 && (
<div className="_wiki-detail-upload-footer">
<Text theme="weak">{pendingFiles.length} </Text>
<Text theme="weak">{pendingFiles.length} file(s) pending upload</Text>
<Button
type="primary"
onClick={handleBatchUpload}
disabled={submitting}
loading={submitting}
>
{submitting ? '上传中…' : '确认上传'}
{submitting ? 'Uploading…' : 'Confirm upload'}
</Button>
</div>
)}
@ -1453,7 +1453,7 @@ export default function WikiSourcesPanel() {
type="text"
onClick={() => setMdDocs((prev) => prev.filter((_, j) => j !== i))}
>
Delete
</Button>
)}
</div>
@ -1466,19 +1466,19 @@ export default function WikiSourcesPanel() {
prev.map((d, j) => (j === i ? { ...d, content: v } : d)),
)
}
placeholder="# 标题"
placeholder="# Title"
/>
</div>
))}
<Button
onClick={() => setMdDocs((prev) => [...prev, { filename: '', content: '' }])}
>
+
+ Add one
</Button>
<div className="_wiki-detail-upload-footer">
<Text theme="weak">
{mdDocs.filter((d) => d.filename.trim() && d.content.trim()).length}{' '}
pending upload
</Text>
<Button
type="primary"
@ -1488,7 +1488,7 @@ export default function WikiSourcesPanel() {
}
loading={submitting}
>
{submitting ? '上传中…' : '确认上传'}
{submitting ? 'Uploading…' : 'Confirm upload'}
</Button>
</div>
</div>
@ -1508,7 +1508,7 @@ export default function WikiSourcesPanel() {
const rejected = all.length - allowed.length;
if (rejected > 0) {
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]);
@ -1528,12 +1528,12 @@ export default function WikiSourcesPanel() {
return (
<div className="_asset-wiki-page">
<AssetPageHeader
title="Wiki 知识库"
title="Wiki knowledge base"
subtitle={
<Text theme="label">
{activeTeam
? `${activeTeam.name} · ${stats.total} 个知识库`
: `${stats.total} 个知识库`}
? `${activeTeam.name} · ${stats.total} knowledge bases total`
: `${stats.total} knowledge bases total`}
</Text>
}
scope={
@ -1554,10 +1554,10 @@ export default function WikiSourcesPanel() {
value={agentFilter}
onChange={setAgentFilter}
disabled={teamAgents.length === 0}
placeholder="无可选 Agent"
placeholder="No agent available"
options={teamAgents.map((agent) => ({
value: agent.id,
text: `${agent.name}${agent.id}`,
text: `${agent.name} (${agent.id})`,
}))}
/>
) : undefined
@ -1567,16 +1567,16 @@ export default function WikiSourcesPanel() {
<Card className="_asset-wiki-content-card">
<Card.Body>
<div className="_asset-wiki-stats">
<MetricsBoard title="知识库总数" value={stats.total} />
<MetricsBoard title="已就绪" value={stats.ready} />
<MetricsBoard title="处理中" value={stats.processing} />
<MetricsBoard title="总页面数" value={stats.totalPages} />
<MetricsBoard title="Total knowledge bases" value={stats.total} />
<MetricsBoard title="Ready" value={stats.ready} />
<MetricsBoard title="Processing" value={stats.processing} />
<MetricsBoard title="Total pages" value={stats.totalPages} />
</div>
<Table.ActionPanel>
<Justify
left={
<Button type="primary" onClick={() => setShowCreate(true)}>
+ Wiki
+ New wiki
</Button>
}
right={
@ -1584,15 +1584,15 @@ export default function WikiSourcesPanel() {
<SearchBox
value={keyword}
onChange={setKeyword}
placeholder="搜索名称 / ID"
placeholder="Search name / ID"
/>
<Segment
value={statusFilter}
onChange={(value) => setStatusFilter(value as StatusFilter)}
options={[
{ value: 'all', text: '全部状态' },
{ value: 'ready', text: '就绪' },
{ value: 'processing', text: '处理中' },
{ value: 'all', text: 'All statuses' },
{ value: 'ready', text: 'Ready' },
{ value: 'processing', text: 'Processing' },
]}
/>
<Segment
@ -1616,13 +1616,13 @@ export default function WikiSourcesPanel() {
emptyText={
<div className="_asset-wiki-empty">
<BooksIcon size="large" />
<Text> Wiki </Text>
<Text theme="label">+ Wiki</Text>
<Text>No wiki knowledge bases</Text>
<Text theme="label">Click "+ New wiki" above to create your first one</Text>
</div>
}
/>
) : filteredSources.length === 0 ? (
<StatusTip status="empty" emptyText="没有匹配的 Wiki试试调整搜索或筛选条件。" />
<StatusTip status="empty" emptyText="No matching wikis. Try adjusting your search or filters." />
) : viewMode === 'card' ? (
<div className="_asset-wiki-grid">
{filteredSources.map((source) => (
@ -1641,20 +1641,20 @@ export default function WikiSourcesPanel() {
<div className="_asset-wiki-card-meta">
<WikiStatusBadge status={source.status} />
<span>
{source.page_count ?? 0} · {formatShortTime(source.last_sync_at)}
{source.page_count ?? 0} pages · {formatShortTime(source.last_sync_at)}
</span>
</div>
<div className="_asset-wiki-card-owner">
<UsergroupIcon size={12} />
{scopeTab === 'fixed' ? (
`固定资产 · ${agentFilter || '未选择 Agent'}`
`Fixed asset · ${agentFilter || 'No agent selected'}`
) : source.owner_user_id ? (
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
) : (
'团队 Wiki 池'
'Team wiki pool'
)}
</div>
<div className="_asset-wiki-card-id">ID{source.wiki_id}</div>
<div className="_asset-wiki-card-id">ID: {source.wiki_id}</div>
<WikiActions
source={source}
scopeTab={scopeTab}
@ -1676,7 +1676,7 @@ export default function WikiSourcesPanel() {
columns={[
{
key: 'name',
header: '名称',
header: 'Name',
width: 240,
render: (source) => (
<button
@ -1692,35 +1692,35 @@ export default function WikiSourcesPanel() {
},
{
key: 'status',
header: '状态',
header: 'Status',
width: 100,
render: (source) => <WikiStatusBadge status={source.status} />,
},
{
key: 'page_count',
header: '页数',
header: 'Pages',
width: 80,
render: (source) => source.page_count ?? 0,
},
{
key: 'owner',
header: '归属',
header: 'Owner',
width: 180,
render: (source) =>
scopeTab === 'fixed' ? (
<span className="_asset-wiki-inline-icon">
<UsergroupIcon size={12} />
{agentFilter || '未选择 Agent'}
{agentFilter || 'No agent selected'}
</span>
) : source.owner_user_id ? (
<WikiOwnerLabel userId={source.owner_user_id} currentUserId={currentUser} />
) : (
<Text theme="label"></Text>
<Text theme="label">Team pool</Text>
),
},
{
key: 'last_sync_at',
header: '最后更新时间',
header: 'Last updated',
width: 140,
render: (source) => (
<Text theme="label">{formatShortTime(source.last_sync_at)}</Text>
@ -1734,7 +1734,7 @@ export default function WikiSourcesPanel() {
},
{
key: 'actions',
header: '操作',
header: 'Actions',
width: 240,
fixed: 'right',
render: (source) => (
@ -1760,19 +1760,19 @@ export default function WikiSourcesPanel() {
{showCreate && (
<Modal
visible
caption="新建 Wiki"
caption="New wiki"
size="s"
onClose={() => setShowCreate(false)}
disableEscape={submitting}
>
<Modal.Body>
<Form>
<Form.Item label="名称" required extra="创建一个新的文档知识库">
<Form.Item label="Name" required extra="Create a new document knowledge base">
<Input
size="full"
value={newName}
onChange={setNewName}
placeholder=" team-docs"
placeholder="e.g. team-docs"
/>
</Form.Item>
</Form>
@ -1784,10 +1784,10 @@ export default function WikiSourcesPanel() {
disabled={submitting || !newName.trim()}
loading={submitting}
>
{submitting ? '创建中…' : '创建'}
{submitting ? 'Creating…' : 'Create'}
</Button>
<Button onClick={() => setShowCreate(false)} disabled={submitting}>
Cancel
</Button>
</Modal.Footer>
</Modal>
@ -1802,9 +1802,9 @@ export default function WikiSourcesPanel() {
team={activeTeam ? { team_id: activeTeam.team_id, name: activeTeam.name } : null}
onClose={() => setAllocateTarget(null)}
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);
tea.notify.success('已分配到 Agent');
tea.notify.success('Assigned to agent');
await fetchSources();
if (scopeTab === 'fixed') await fetchFixedBindings();
}}
@ -1837,11 +1837,11 @@ function WikiActions({
return (
<div className="_asset-wiki-actions" onClick={(event) => event.stopPropagation()}>
<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>
{scopeTab === 'fixed' ? (
<Button type="weak" onClick={() => onUnbind(source.wiki_id)}>
Unbind
</Button>
) : (
<Button
@ -1850,16 +1850,16 @@ function WikiActions({
tooltip={
source.status === 'ready'
? 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 })}
>
Assign
</Button>
)}
<Button
type="icon"
tooltip="删除"
tooltip="Delete"
onClick={() => onDelete(source.wiki_id, source.name)}
>
<DeleteIcon size={14} />
@ -1951,7 +1951,7 @@ function GraphTabContent({
) : (
<div className="_wiki-detail-side-empty">
<ArchitectureIcon size="large" />
<Text theme="label"></Text>
<Text theme="label">Click a node to view its content</Text>
</div>
)}
</div>
@ -2010,7 +2010,7 @@ function PagesTabContent({
className={`_wiki-detail-filter-tag${pageTypeFilter === 'all' ? ' is-active' : ''}`}
onClick={() => setPageTypeFilter('all')}
>
{allPages.length}
All {allPages.length}
</button>
{types.map((type) => (
<button
@ -2047,9 +2047,9 @@ function PagesTabContent({
type="text"
className="_wiki-detail-page-delete"
onClick={() => onDeletePage(page)}
tooltip="删除页面"
tooltip="Delete page"
>
Delete
</Button>
</div>
);
@ -2087,7 +2087,7 @@ function PagesTabContent({
{tag.trim()}
</Tag>
))}
{metadata.created && <Text theme="label">{metadata.created}</Text>}
{metadata.created && <Text theme="label">Created: {metadata.created}</Text>}
</div>
)}
{readLoading ? (
@ -2105,7 +2105,7 @@ function PagesTabContent({
) : (
<div className="_wiki-detail-side-empty">
<BooksIcon size="large" />
<Text theme="label"></Text>
<Text theme="label">Select a page on the left to view its content</Text>
</div>
)}
</div>
@ -2140,7 +2140,7 @@ function RawFilesSection({
knowledgeApi.wiki
.rawList(wikiId)
.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));
}, [wikiId]);
@ -2158,7 +2158,7 @@ function RawFilesSection({
if (loading)
return (
<div className="_wiki-detail-rawfiles-loading">
<FolderIcon size={12} />
<FolderIcon size={12} /> Loading original documents
</div>
);
if (files.length === 0) return null;
@ -2167,7 +2167,7 @@ function RawFilesSection({
<div className="_wiki-detail-rawfiles">
<button className="_wiki-detail-rawfiles-toggle" onClick={() => setExpanded(!expanded)}>
<span>
<FolderIcon size={12} /> {files.length}
<FolderIcon size={12} /> Original documents ({files.length})
</span>
<ChevronRightIcon size={12} className={expanded ? 'is-open' : ''} />
</button>
@ -2184,9 +2184,9 @@ function RawFilesSection({
type="text"
className="_wiki-detail-page-delete"
onClick={() => void handleDelete(file.filename)}
tooltip="删除原始文档"
tooltip="Delete original document"
>
Delete
</Button>
</div>
))}
@ -2213,7 +2213,7 @@ function KnowledgeGraphEmbed({
highlightNode: string | null;
}) {
return (
<Suspense fallback={<StatusTip status="loading" loadingText="加载图谱组件…" />}>
<Suspense fallback={<StatusTip status="loading" loadingText="Loading graph component…" />}>
<KnowledgeGraphLazy
data={data}
loading={loading}

View File

@ -69,14 +69,14 @@ export default function TaskCreateDialog(props: {
}
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>
<Form>
<Form.Item label="所属 Team">
<Form.Item label="Team">
<div className="_memory-tcd-team-row">
<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-label"> team</div>
<div className="_memory-tcd-team-label">Will be created in team</div>
<div className="_memory-tcd-team-name-row">
<span className="_memory-tcd-team-name">{props.team.name}</span>
<Tag size="sm">{props.team.team_id}</Tag>
@ -84,30 +84,30 @@ export default function TaskCreateDialog(props: {
</div>
</div>
</Form.Item>
<Form.Item label="标题" required>
<Form.Item label="Title" required>
<Input
autoFocus
size="full"
value={title}
onChange={setTitle}
placeholder="例如:修复 #142 macOS 14 启动失败"
placeholder="e.g. Fix #142 macOS 14 startup failure"
/>
</Form.Item>
<Form.Item label="描述" required extra="关联 Agent 可在创建后再挂载">
<Form.Item label="Description" required extra="Linked agents can be attached after creation">
<Input.TextArea
size="full"
value={description}
onChange={setDescription}
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>
{error && <Form.Item><Alert type="error">{error}</Alert></Form.Item>}
</Form>
</Modal.Body>
<Modal.Footer>
<Button type="primary" onClick={() => void submit()} disabled={!canSubmit} loading={submitting}> Task</Button>
<Button onClick={props.onClose} disabled={submitting}></Button>
<Button type="primary" onClick={() => void submit()} disabled={!canSubmit} loading={submitting}>Create task</Button>
<Button onClick={props.onClose} disabled={submitting}>Cancel</Button>
</Modal.Footer>
</Modal>
);

View File

@ -44,8 +44,8 @@ function errMsg(e: unknown): string {
// 历史的 待处理 / 阻塞 / 已归档 已下线(参见 backendStore.ts 里的 normalizeTaskStatus
const STATUS_LABEL: Record<Task['status'], string> = {
running: '进行中',
completed: '已完成'
running: 'In progress',
completed: 'Completed'
};
// Tag 组件合法 theme: default/primary/success/warning/error
@ -172,7 +172,7 @@ export default function TaskWorkbench(props: {
// 谁点击「创建 Task」谁就是 creator_user_id。
const team = teams.find((t) => t.team_id === draft.team_id);
if (!team) {
tea.notify.error(`team「${draft.team_id}」不存在,无法创建 task。`);
tea.notify.error(`Team "${draft.team_id}" does not exist. Cannot create task.`);
return;
}
try {
@ -207,15 +207,15 @@ export default function TaskWorkbench(props: {
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
if (!canDeleteTask(task, team, currentUser) && !isAdmin) {
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;
}
const ok = await tea.confirm({
message: `确认删除 task「${task.title}」?`,
message: `Delete task "${task.title}"?`,
description: `Task ID: ${task.task_id}`,
okText: '删除',
cancelText: '取消',
okText: 'Delete',
cancelText: 'Cancel',
});
if (ok) {
try {
@ -230,7 +230,7 @@ export default function TaskWorkbench(props: {
// 权限:编辑 task含切换 status允许 team 内任意 member / admin
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
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;
}
try {
@ -242,7 +242,7 @@ export default function TaskWorkbench(props: {
onUpdateTask={async (task, patch) => {
const team = teams.find((t) => t.team_id === task.team_id) ?? null;
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;
}
try {
@ -278,9 +278,9 @@ function EmptyTeam() {
return (
<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">
team task
Create a team in "Team management" first, then come back to the workbench to create a task.
</Text>
</Card.Body>
</Card>
@ -321,15 +321,15 @@ function BoardView({
<Card className="_memory-workbench-card">
<Card.Body className="_memory-workbench-list-body">
<div className="_memory-workbench-list-header">
<Text theme="strong">Task </Text>
<Text theme="strong">Task list</Text>
<Button type="primary" onClick={onCreate}>
<AddIcon size={14} />
Task
New task
</Button>
</div>
{tasks.length === 0 ? (
<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>
) : (
<List split="divide" className="_memory-workbench-list-items">
@ -361,7 +361,7 @@ function BoardView({
</Text>
{canDelete && (
<Button type="text"
tooltip="删除该 Task"
tooltip="Delete this task"
className="_memory-workbench-item-delete"
onClick={(e) => {
e?.stopPropagation();
@ -379,16 +379,16 @@ function BoardView({
session user hover user_id */}
<span
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} />
{view.users.length}
{imParticipant && <Tag theme="warning" variant="soft" size="sm"></Tag>}
{view.users.length} people
{imParticipant && <Tag theme="warning" variant="soft" size="sm">Includes you</Tag>}
</span>
{/* agent session agent
hover agent name / agent_id */}
<span title={agentLabels.length === 0 ? '暂无实际参与的 Agent' : `实际参与 Agent\n${agentLabels.join('\n')}`}>
{agentLabels.length} Agent
<span title={agentLabels.length === 0 ? 'No agents have actually participated yet' : `Actual participating agents:\n${agentLabels.join('\n')}`}>
{agentLabels.length} agents
</span>
<span className="_memory-workbench-item-time">
{new Date(t.updated_at_ms).toLocaleString()}
@ -409,7 +409,7 @@ function BoardView({
<Card.Body className="_memory-workbench-detail-body">
{!selected ? (
<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>
) : (
<TaskDetail
@ -478,7 +478,7 @@ function TaskDetail({
const patch: Partial<Pick<Task, 'title' | 'description' | 'source_type' | 'source_url' | 'linked_agents'>> = {};
const title = draftTitle.trim();
if (title.length === 0) {
tea.notify.warning('任务标题不能为空。');
tea.notify.warning('Task title cannot be empty.');
return;
}
if (title !== task.title) patch.title = title;
@ -514,7 +514,7 @@ function TaskDetail({
<Input
value={draftTitle}
onChange={setDraftTitle}
placeholder="任务标题"
placeholder="Task title"
size="full"
className="_memory-workbench-title-input"
/>
@ -522,35 +522,35 @@ function TaskDetail({
<Text theme="strong" className="_memory-workbench-detail-title">{task.title}</Text>
)}
<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>
<span className="_memory-workbench-meta-sep">·</span>
<Text theme="weak">team</Text>
<Text theme="weak">team:</Text>
{team ? (
<>
<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 className="_memory-workbench-detail-actions">
{/* 编辑按钮:仅 team 成员可见可点;编辑态下隐藏,由保存/取消替代 */}
{!editing && canEdit && (
<Button onClick={startEdit} tooltip="编辑任务详情(标题、描述)">
<Button onClick={startEdit} tooltip="Edit task details (title, description)">
<EditIcon size={14} />
Edit
</Button>
)}
{editing && (
<>
<Button onClick={cancelEdit}></Button>
<Button type="primary" onClick={saveEdit}></Button>
<Button onClick={cancelEdit}>Cancel</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) => {
const active = task.status === s;
return (
@ -574,18 +574,18 @@ function TaskDetail({
fire-and-forget append append-only Set */}
<div className="_memory-workbench-people">
<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
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} />
<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>
</div>
<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 ? (
<Text theme="weak"></Text>
) : (
@ -593,17 +593,17 @@ function TaskDetail({
<span
key={u}
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} />
<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>
))
)}
</div>
<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 ? (
<Text theme="weak"></Text>
) : (
@ -611,7 +611,7 @@ function TaskDetail({
<span
key={a.id}
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} />
<Text theme="text">{a.name}</Text>
@ -623,14 +623,14 @@ function TaskDetail({
{/* === 描述 === */}
<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 ? (
<Input.TextArea
value={draftDesc}
onChange={setDraftDesc}
rows={6}
size="full"
placeholder="包含背景、目标、验收标准…"
placeholder="Include background, goals, acceptance criteria…"
/>
) : (
<pre className="_memory-workbench-desc-view">{task.description}</pre>
@ -641,7 +641,7 @@ function TaskDetail({
task-agent/link */}
<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>
</div>
);

View File

@ -90,11 +90,11 @@ export function findAccountByUsername(username: string): MockAccount | null {
/** 校验邮箱 + 密码登录 */
export function verifyAccountCredentials(email: string, password: string): MockAccount {
const e = email.trim().toLowerCase();
if (!e) throw new Error('请输入邮箱。');
if (!password) throw new Error('请输入密码。');
if (!e) throw new Error('Please enter an email address.');
if (!password) throw new Error('Please enter a password.');
const account = readAccounts().find((a) => a.email.toLowerCase() === e);
if (!account) throw new Error(`账号不存在:${e}`);
if (account.password !== password) throw new Error('密码错误。');
if (!account) throw new Error(`Account not found: ${e}`);
if (account.password !== password) throw new Error('Incorrect password.');
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 {
const e = input.email.trim().toLowerCase();
if (!e) throw new Error('邮箱不能为空。');
if (!input.username.trim()) throw new Error('用户名不能为空。');
if (!e) throw new Error('Email cannot be empty.');
if (!input.username.trim()) throw new Error('Username cannot be empty.');
const accounts = readAccounts();
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 = {
email: input.email.trim(),
@ -133,11 +133,11 @@ export function batchCreateAccounts(
const e = entry.email.trim().toLowerCase();
const u = entry.username.trim();
if (!e || !u) {
errors.push({ email: entry.email || '(空)', error: '邮箱和用户名都不能为空' });
errors.push({ email: entry.email || '(empty)', error: 'Email and username cannot be empty' });
continue;
}
if (emailSet.has(e)) {
errors.push({ email: entry.email, error: '邮箱已被注册' });
errors.push({ email: entry.email, error: 'Email is already registered' });
continue;
}
const account: MockAccount = {
@ -160,13 +160,13 @@ export function batchCreateAccounts(
/** 修改密码 */
export function changePassword(username: string, oldPassword: string, newPassword: string): void {
if (!oldPassword) throw new Error('请输入当前密码。');
if (!newPassword) throw new Error('请输入新密码。');
if (newPassword.length < 4) throw new Error('新密码至少需要 4 位。');
if (!oldPassword) throw new Error('Please enter your current password.');
if (!newPassword) throw new Error('Please enter a new password.');
if (newPassword.length < 4) throw new Error('New password must be at least 4 characters.');
const accounts = readAccounts();
const account = accounts.find((a) => a.username === username);
if (!account) throw new Error('账号不存在。');
if (account.password !== oldPassword) throw new Error('当前密码错误。');
if (!account) throw new Error('Account not found.');
if (account.password !== oldPassword) throw new Error('Current password is incorrect.');
account.password = newPassword;
writeAccountsRaw(accounts);
}
@ -176,12 +176,12 @@ export function changePassword(username: string, oldPassword: string, newPasswor
* UI admin
*/
export function setAccountPassword(username: string, newPassword: string): void {
if (!newPassword) throw new Error('请输入新密码。');
if (newPassword.length < 4) throw new Error('新密码至少需要 4 位。');
if (!username) throw new Error('用户名不能为空。');
if (!newPassword) throw new Error('Please enter a new password.');
if (newPassword.length < 4) throw new Error('New password must be at least 4 characters.');
if (!username) throw new Error('Username cannot be empty.');
const accounts = readAccounts();
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;
writeAccountsRaw(accounts);
}
@ -189,14 +189,14 @@ export function setAccountPassword(username: string, newPassword: string): void
/** 修改用户邮箱admin 专有权限,权限校验在 UI 层) */
export function updateAccountEmail(username: string, newEmail: string): void {
const e = newEmail.trim().toLowerCase();
if (!e) throw new Error('邮箱不能为空。');
if (!username) throw new Error('用户名不能为空。');
if (!e) throw new Error('Email cannot be empty.');
if (!username) throw new Error('Username cannot be empty.');
const accounts = readAccounts();
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);
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();
writeAccountsRaw(accounts);
}

View File

@ -40,11 +40,11 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
{
template_id: 'builtin-pr-reviewer',
name: 'PR Reviewer',
summary: '代码合入主干前的最后一道质量关卡',
summary: 'The last quality gate before code merges to main',
builtin: true,
description: '执行 pr-review workflow核对必查项 + 给出 actionable 评论。',
role_prompt: '你是严格的 PR Reviewer是代码合入主干前的最后一道质量关卡。',
rules_prompt: '1. 先读 PR 描述与关联 issue明确改动意图。\n2. 必查:正确性、边界条件、安全、测试覆盖、命名与可读性。\n3. 每条评论必须 actionable指明位置与建议改法。\n4. 阻断性问题与建议性问题分开标注。',
description: 'Runs the pr-review workflow: checks required items and leaves actionable comments.',
role_prompt: 'You are a strict PR Reviewer, the last quality gate before code merges to main.',
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: [],
code_graphs: [],
llm_wikis: [],
@ -53,12 +53,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
},
{
template_id: 'builtin-bugfix-engineer',
name: 'Bug-fix 工程师',
summary: '复现 → 定位 → 修复 → 自测的修复 loop',
name: 'Bug-fix Engineer',
summary: 'Reproduce → locate → fix → self-test loop',
builtin: true,
description: '面向 bug-fix loop 的工程师 agent复现 → 定位 → 修复 → 自测。',
role_prompt: '你是面向 bug-fix loop 的修复工程师,对每个缺陷负责到根因,追求最小且可验证的修复。',
rules_prompt: '1. 先稳定复现,再动手;无法复现先补复现信息。\n2. 定位根因而非掩盖症状。\n3. 修复保持最小 diff附带回归测试。\n4. 自测通过后再提交,说明验证方式。',
description: 'An engineer agent for the bug-fix loop: reproduce → locate → fix → self-test.',
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. 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: [],
code_graphs: [],
llm_wikis: [],
@ -67,12 +67,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
},
{
template_id: 'builtin-issue-triage',
name: 'Issue 分诊员',
summary: '新进 issue 的第一接待人:分类 / 补全 / 指派',
name: 'Issue Triager',
summary: 'First responder for incoming issues: classify / complete / assign',
builtin: true,
description: '面向新进 issue判断类型、补充复现信息、指派 owner。',
role_prompt: '你是 issue 分诊员,是新进 issue 的第一接待人,负责分类、补全信息并指派。',
rules_prompt: '1. 判断类型bug / feature / question / 重复)。\n2. 缺信息时按模板向报告者追问复现步骤、环境、期望。\n3. 标注优先级与影响面。\n4. 指派合适 owner 并说明理由。',
description: 'For incoming issues: determines the type, fills in missing repro info, and assigns an owner.',
role_prompt: 'You are an issue triager, the first responder for incoming issues, responsible for classifying, completing information, and assigning them.',
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: [],
code_graphs: [],
llm_wikis: [],
@ -81,12 +81,12 @@ const BUILTIN_AGENT_TEMPLATES: AgentTemplate[] = [
},
{
template_id: 'builtin-doc-engineer',
name: '文档工程师',
summary: '随 PR 同步更新 wiki / changelog',
name: 'Documentation Engineer',
summary: 'Keeps the wiki / changelog updated alongside each PR',
builtin: true,
description: '随 PR 同步更新 wiki / changelog保持团队知识库与代码一致。',
role_prompt: '你是文档工程师,确保团队知识库与代码始终保持同步、可信。',
rules_prompt: '1. 每个会影响行为的 PR 都要评估文档影响。\n2. 更新 changelog语言面向使用者而非实现者。\n3. 失效文档及时下线或标注。\n4. 文档需可被检索,附必要链接与示例。',
description: 'Updates wiki / changelog alongside each PR, keeping the team knowledge base consistent with the code.',
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. 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: [],
code_graphs: [],
llm_wikis: [],
@ -128,7 +128,7 @@ export function createAgentTemplate(input: {
chat_memories?: string[];
}): AgentTemplate {
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 tpl: AgentTemplate = {
template_id: `tpl_${now}_${Math.random().toString(36).slice(2, 8)}`,

View File

@ -118,7 +118,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
} catch (err) {
console.error('[backend store] fetchTeams failed:', err);
set({ teamsLoading: false });
tea.notify.error('加载团队列表失败');
tea.notify.error('Failed to load team list');
} finally {
set({ inflightTeams: null });
}
@ -158,7 +158,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
Object.entries(s.inflightAgents).filter(([k]) => k !== teamId)
),
}));
tea.notify.error('加载 Agent 列表失败');
tea.notify.error('Failed to load agent list');
return [];
}
})();
@ -204,7 +204,7 @@ export const useBackendStore = create<BackendState>((set, get) => ({
Object.entries(s.inflightTasks).filter(([k]) => k !== teamId)
),
}));
tea.notify.error('加载任务列表失败');
tea.notify.error('Failed to load task list');
return [];
}
})();

View File

@ -41,6 +41,19 @@ fi
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_*
# LLM_MODE=custom → 不走 memory 的 LLM proxy而是 knowledge 直连用户提供的端点
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 "${KNOWLEDGE_PORT}:8424" \
-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 KNOWLEDGE_PORT=8424 \
-e KNOWLEDGE_PUBLIC_BASE_URL="$KNOWLEDGE_PUBLIC_BASE_URL" \

View File

@ -1,7 +1,7 @@
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 || \
sed -i 's|deb.debian.org|mirrors.tencent.com|g' /etc/apt/sources.list 2>/dev/null || true
# GODCALL: upstream rewrote apt sources to mirrors.tencent.com here — unreachable
# outside Tencent's network; use stock Debian mirrors.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ git curl ca-certificates \

87
tools/import-gitea.mjs Normal file
View 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`);