Prechádzať zdrojové kódy

feat: 内置组长 Profile 与 team-lead 技能;任务结束时保存任务纪要

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 1 týždeň pred
rodič
commit
153881bb20

+ 13 - 0
apps/admin/src/views/BotsView.vue

@@ -73,6 +73,16 @@ function openBot(b?: BotDto) {
   editor.open = true
 }
 
+// 切换类型时,如果还在用内置的通用助手 / 组长 Profile,换成对应类型的内置 Profile(同一引擎)
+watch(() => [editor.form, editor.form.kind] as const, ([form, kind], [prevForm]) => {
+  // 打开另一个机器人时整个表单被替换,不是在修改类型
+  if (form !== prevForm) return
+  const cur = profiles.value.find(p => p.id === editor.form.profileId)
+  if (!cur?.builtin) return
+  const target = profiles.value.find(p => p.builtin && p.provider === cur.provider && (kind === 'lead' ? p.builtinKey === 'lead' : !p.builtinKey))
+  if (target) editor.form.profileId = target.id
+})
+
 async function saveBot() {
   if (!formValid.value) return
   editor.saving = true
@@ -212,6 +222,9 @@ onMounted(load)
             <option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}({{ p.provider }} / {{ p.model }} · {{ PERMISSION_LABELS[p.permissionMode] }})</option>
           </select>
           <div class="hint mt-1">技能、MCP 服务、资料库、权限模式都由 Profile 决定。工作组中的机器人尽量用"自动执行"或"只读",避免频繁审批。</div>
+          <div v-if="editor.form.kind === 'lead' && profiles.find(p => p.id === editor.form.profileId)?.builtinKey !== 'lead'" class="hint mt-1 text-warning">
+            组长的协作规范(怎么拆分、验收、交付)写在内置的"组长"Profile 中。使用其他 Profile 时,请先从"组长"Profile 复制,再在副本上修改。
+          </div>
         </div>
         <label class="flex items-center gap-2 cursor-pointer"><input v-model="editor.form.enabled" type="checkbox">启用</label>
         <div class="flex justify-end gap-2">

+ 1 - 1
apps/admin/src/views/ProfilesView.vue

@@ -40,7 +40,7 @@ const blank = (): ProfileInput => ({
 })
 
 function toInput(p: ProfileDto): ProfileInput {
-  const { id: _id, version: _v, updatedAt: _u, builtin: _b, ...rest } = clone(p)
+  const { id: _id, version: _v, updatedAt: _u, builtin: _b, builtinKey: _k, ...rest } = clone(p)
   return rest
 }
 

+ 1 - 0
apps/server/drizzle/0009_lead_profile.sql

@@ -0,0 +1 @@
+ALTER TABLE `profiles` ADD `builtin_key` text;

+ 2336 - 0
apps/server/drizzle/meta/0009_snapshot.json

@@ -0,0 +1,2336 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "07f8d658-b99a-4e12-a247-7c9d6f9378ab",
+  "prevId": "f71bd72e-f1e8-45f0-a3a7-66eb3cd051ee",
+  "tables": {
+    "admin_sessions": {
+      "name": "admin_sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "api_keys": {
+      "name": "api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_profile_ids": {
+          "name": "allowed_profile_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "default_profile_id": {
+          "name": "default_profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "api_keys_key_hash_unique": {
+          "name": "api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "audit_logs": {
+      "name": "audit_logs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "integer",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": true
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "actor": {
+          "name": "actor",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "detail": {
+          "name": "detail",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "audit_ts_idx": {
+          "name": "audit_ts_idx",
+          "columns": [
+            "ts"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bot_api_keys": {
+      "name": "bot_api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_bot_ids": {
+          "name": "allowed_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "bot_api_keys_key_hash_unique": {
+          "name": "bot_api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bots": {
+      "name": "bots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "avatar": {
+          "name": "avatar",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'🤖'"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "bots_name_unique": {
+          "name": "bots_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skill_versions": {
+      "name": "custom_skill_versions",
+      "columns": {
+        "skill_id": {
+          "name": "skill_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reason": {
+          "name": "reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "changed": {
+          "name": "changed",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "custom_skill_versions_skill_id_version_pk": {
+          "columns": [
+            "skill_id",
+            "version"
+          ],
+          "name": "custom_skill_versions_skill_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skills": {
+      "name": "custom_skills",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source_id": {
+          "name": "source_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "include_in_all": {
+          "name": "include_in_all",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "published_version": {
+          "name": "published_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "latest_version": {
+          "name": "latest_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "working_files": {
+          "name": "working_files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "custom_skills_name_unique": {
+          "name": "custom_skills_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "events": {
+      "name": "events",
+      "columns": {
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "events_session_id_seq_pk": {
+          "columns": [
+            "session_id",
+            "seq"
+          ],
+          "name": "events_session_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "files": {
+      "name": "files",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "rel_path": {
+          "name": "rel_path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mime": {
+          "name": "mime",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "size": {
+          "name": "size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mtime_ms": {
+          "name": "mtime_ms",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "files_session_path_idx": {
+          "name": "files_session_path_idx",
+          "columns": [
+            "session_id",
+            "rel_path"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "interactions": {
+      "name": "interactions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "request": {
+          "name": "request",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "response": {
+          "name": "response",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "resolved_at": {
+          "name": "resolved_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "interactions_session_idx": {
+          "name": "interactions_session_idx",
+          "columns": [
+            "session_id",
+            "status"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "jobs": {
+      "name": "jobs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "plan": {
+          "name": "plan",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'running'"
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_runs": {
+          "name": "lead_runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "run_ids": {
+          "name": "run_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "jobs_team_idx": {
+          "name": "jobs_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "knowledge_roots": {
+      "name": "knowledge_roots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "access": {
+          "name": "access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'read'"
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "mcp_servers": {
+      "name": "mcp_servers",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "transport": {
+          "name": "transport",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "headers": {
+          "name": "headers",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "command": {
+          "name": "command",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "args": {
+          "name": "args",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "env": {
+          "name": "env",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "allow_network": {
+          "name": "allow_network",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "read_paths": {
+          "name": "read_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "write_paths": {
+          "name": "write_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tool_policies": {
+          "name": "tool_policies",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "tools_cache": {
+          "name": "tools_cache",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tools_updated_at": {
+          "name": "tools_updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "mcp_servers_name_unique": {
+          "name": "mcp_servers_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profile_versions": {
+      "name": "profile_versions",
+      "columns": {
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "snapshot": {
+          "name": "snapshot",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "profile_versions_profile_id_version_pk": {
+          "columns": [
+            "profile_id",
+            "version"
+          ],
+          "name": "profile_versions_profile_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profiles": {
+      "name": "profiles",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "system_prompt": {
+          "name": "system_prompt",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'ask'"
+        },
+        "allowed_tools": {
+          "name": "allowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "disallowed_tools": {
+          "name": "disallowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "skills": {
+          "name": "skills",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'\"all\"'"
+        },
+        "file_access": {
+          "name": "file_access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "knowledge_root_ids": {
+          "name": "knowledge_root_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "mcp_server_ids": {
+          "name": "mcp_server_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin": {
+          "name": "builtin",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin_key": {
+          "name": "builtin_key",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 1
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "runs": {
+      "name": "runs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "error": {
+          "name": "error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "runs_session_idx": {
+          "name": "runs_session_idx",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "sessions": {
+      "name": "sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "system_prompt_append": {
+          "name": "system_prompt_append",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider_session_id": {
+          "name": "provider_session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "archived": {
+          "name": "archived",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "sessions_source_idx": {
+          "name": "sessions_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "settings": {
+      "name": "settings",
+      "columns": {
+        "key": {
+          "name": "key",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "value": {
+          "name": "value",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "skill_sources": {
+      "name": "skill_sources",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "sync_mode": {
+          "name": "sync_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'manual'"
+        },
+        "interval_min": {
+          "name": "interval_min",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 10
+        },
+        "last_sync_at": {
+          "name": "last_sync_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "last_sync_digest": {
+          "name": "last_sync_digest",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'never'"
+        },
+        "last_error": {
+          "name": "last_error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "tasks": {
+      "name": "tasks",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "job_id": {
+          "name": "job_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "instructions": {
+          "name": "instructions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "depends_on": {
+          "name": "depends_on",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "tasks_team_idx": {
+          "name": "tasks_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        },
+        "tasks_job_idx": {
+          "name": "tasks_job_idx",
+          "columns": [
+            "job_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bot_sessions": {
+      "name": "team_bot_sessions",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "last_read_seq": {
+          "name": "last_read_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        }
+      },
+      "indexes": {
+        "team_bot_sessions_session_id_unique": {
+          "name": "team_bot_sessions_session_id_unique",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bot_sessions_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bot_sessions_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bots": {
+      "name": "team_bots",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "joined_at": {
+          "name": "joined_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "team_bots_bot_idx": {
+          "name": "team_bots_bot_idx",
+          "columns": [
+            "bot_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bots_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bots_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_cases": {
+      "name": "team_cases",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_bot_id": {
+          "name": "lead_bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "worker_bot_ids": {
+          "name": "worker_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "stub_bot_ids": {
+          "name": "stub_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "human_name": {
+          "name": "human_name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "text": {
+          "name": "text",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mentions": {
+          "name": "mentions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_events": {
+      "name": "team_events",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_events_team_id_seq_pk": {
+          "columns": [
+            "team_id",
+            "seq"
+          ],
+          "name": "team_events_team_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "teams": {
+      "name": "teams",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "human": {
+          "name": "human",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'active'"
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "debug": {
+          "name": "debug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{\"stepMode\":false,\"stubBotIds\":[]}'"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "case_id": {
+          "name": "case_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "teams_source_idx": {
+          "name": "teams_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        },
+        "teams_case_idx": {
+          "name": "teams_case_idx",
+          "columns": [
+            "case_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "usage_daily": {
+      "name": "usage_daily",
+      "columns": {
+        "date": {
+          "name": "date",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "runs": {
+          "name": "runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "failed": {
+          "name": "failed",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "interrupted": {
+          "name": "interrupted",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_daily_date_source_provider_model_pk": {
+          "columns": [
+            "date",
+            "source",
+            "provider",
+            "model"
+          ],
+          "name": "usage_daily_date_source_provider_model_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}

+ 7 - 0
apps/server/drizzle/meta/_journal.json

@@ -64,6 +64,13 @@
       "when": 1793079581381,
       "tag": "0008_team_cases",
       "breakpoints": true
+    },
+    {
+      "idx": 9,
+      "version": "6",
+      "when": 1793084140773,
+      "tag": "0009_lead_profile",
+      "breakpoints": true
     }
   ]
 }

+ 2 - 0
apps/server/src/app.ts

@@ -31,6 +31,7 @@ import { FileManagerService } from './services/file-manager'
 import { BrowserTokenService } from './services/browser-tokens'
 import { BotKeyService, BotService } from './teams/bots'
 import { TeamCaseService } from './teams/cases'
+import { ensureLeadProfiles } from './teams/lead-profile'
 import { CardService } from './teams/cards'
 import { TeamService } from './teams/service'
 import { createSkillCatalog } from './services/skill-catalog'
@@ -82,6 +83,7 @@ export function createApp(log: AppLogger, opts: { db?: DB; providers?: Partial<R
   sessions.extraModels = p => providers[p]?.knownModels?.() ?? []
   const runs = new RunManager({ db, log, settings, sessions, profiles, knowledge, files, events, interactions, audit, usage, plugins, communitySkills, mcpServers, customSkills, gatewayTokens, providers })
   const bots = new BotService(db, profiles)
+  ensureLeadProfiles({ profiles, customSkills, bots, settings })
   const botKeys = new BotKeyService(db, bots)
   usage.extraSourceNames = () => botKeys.names()
   const skillCatalog = createSkillCatalog({ settings: () => settings.get(), plugins, communitySkills, customSkills, providers })

+ 3 - 1
apps/server/src/db/schema.ts

@@ -37,8 +37,10 @@ export const profiles = sqliteTable('profiles', {
   knowledgeRootIds: text('knowledge_root_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
   mcpServerIds: text('mcp_server_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
   isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
-  /** 内置 Profile(通用助手)不可删除 */
+  /** 内置 Profile(通用助手、组长)不可删除 */
   builtin: integer('builtin', { mode: 'boolean' }).notNull().default(false),
+  /** 内置 Profile 的种类:空为通用助手,lead 为组长(docs/design/13-bot-teams.md §17) */
+  builtinKey: text('builtin_key').$type<'lead'>(),
   version: integer('version').notNull().default(1),
   createdAt: createdAt(),
   updatedAt: updatedAt(),

+ 12 - 6
apps/server/src/services/catalog.ts

@@ -1,6 +1,6 @@
 // Profile、资料库、Agent Key、审计:后台管理的配置类数据
 import crypto from 'node:crypto'
-import { and, desc, eq, ne } from 'drizzle-orm'
+import { and, desc, eq, isNull, ne } from 'drizzle-orm'
 import { z } from 'zod'
 import type { ApiKeyDto, ApiKeyProfiles, FileAccessPolicy, KnowledgeRootDto, ProfileDto, ProfileInput, ProfileSnapshot, ProfileVersionDto, ProviderId } from '@agent-server/shared'
 import type { DB } from '../db'
@@ -39,7 +39,7 @@ export const profileInputSchema: z.ZodType<ProfileInput> = z.object({
 const toProfileDto = (r: ProfileRow): ProfileDto => ({
   id: r.id, name: r.name, description: r.description, provider: r.provider, model: r.model, systemPrompt: r.systemPrompt,
   permissionMode: r.permissionMode, allowedTools: r.allowedTools, disallowedTools: r.disallowedTools, skills: r.skills,
-  fileAccess: r.fileAccess, knowledgeRootIds: r.knowledgeRootIds, mcpServerIds: r.mcpServerIds, isDefault: r.isDefault, builtin: r.builtin, version: r.version, updatedAt: r.updatedAt,
+  fileAccess: r.fileAccess, knowledgeRootIds: r.knowledgeRootIds, mcpServerIds: r.mcpServerIds, isDefault: r.isDefault, builtin: r.builtin, builtinKey: r.builtinKey ?? null, version: r.version, updatedAt: r.updatedAt,
 })
 
 const SNAPSHOT_KEYS = ['name', 'description', 'provider', 'model', 'systemPrompt', 'permissionMode', 'allowedTools', 'disallowedTools', 'skills', 'fileAccess', 'knowledgeRootIds', 'mcpServerIds'] as const satisfies readonly (keyof ProfileSnapshot)[]
@@ -66,7 +66,7 @@ export class ProfileService {
   private ensureBuiltins() {
     const hasAny = !!this.db.select({ id: profiles.id }).from(profiles).limit(1).get()
     for (const provider of ['claude', 'codex'] as const) {
-      const existing = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider))).get()
+      const existing = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider), isNull(profiles.builtinKey))).get()
       if (existing) {
         if (existing.name === '通用助手') this.db.update(profiles).set({ name: BUILTIN_NAMES[provider] }).where(eq(profiles.id, existing.id)).run()
         continue
@@ -116,13 +116,13 @@ export class ProfileService {
     if (missing.length) throw badRequest(`MCP 服务不存在:${missing.join('、')}`)
   }
 
-  create(input: ProfileInput, builtin = false): ProfileDto {
+  create(input: ProfileInput, builtin = false, builtinKey: 'lead' | null = null): ProfileDto {
     const data = parse(profileInputSchema, input)
     this.checkMcp(data.mcpServerIds)
     const id = newId('prf')
     this.db.transaction(tx => {
       if (data.isDefault) tx.update(profiles).set({ isDefault: false }).run()
-      tx.insert(profiles).values({ id, ...data, builtin }).run()
+      tx.insert(profiles).values({ id, ...data, builtin, builtinKey }).run()
       tx.insert(profileVersions).values({ profileId: id, version: 1, snapshot: snapshotOf(data) }).run()
     })
     return this.get(id)
@@ -165,10 +165,16 @@ export class ProfileService {
   }
 
   getBuiltin(provider: ProviderId = 'claude'): ProfileDto {
-    const row = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider))).get()
+    const row = this.db.select().from(profiles).where(and(eq(profiles.builtin, true), eq(profiles.provider, provider), isNull(profiles.builtinKey))).get()
     return row ? toProfileDto(row) : this.getDefault()
   }
 
+  /** 内置的组长 Profile(没有时返回 undefined) */
+  findBuiltinLead(provider: ProviderId): ProfileDto | undefined {
+    const row = this.db.select().from(profiles).where(and(eq(profiles.builtinKey, 'lead'), eq(profiles.provider, provider))).get()
+    return row ? toProfileDto(row) : undefined
+  }
+
   delete(id: string) {
     const p = this.get(id)
     if (p.builtin) throw badRequest('内置的 Profile 不能删除')

+ 126 - 0
apps/server/src/teams/lead-profile.ts

@@ -0,0 +1,126 @@
+// 内置的组长 Profile 与 team-lead 技能(docs/design/13-bot-teams.md §17)。
+// 组长的"协作规范"(什么时候开任务、怎么拆分、怎么验收和交付)放在 Profile 的系统提示词里,后台可以修改或复制;
+// 更详细的做法(说明模板、验收清单、纪要格式)放在自定义技能 team-lead 中。和协作工具绑定的机制说明仍由服务端生成(prompt.ts)
+import type { ProviderId } from '@agent-server/shared'
+import { skillId, type CustomSkillService } from '../custom-skills/service'
+import type { ProfileService } from '../services/catalog'
+import type { SettingsService } from '../services/settings'
+import type { BotService } from './bots'
+
+export const LEAD_SKILL = 'team-lead'
+const LEAD_NAMES: Record<ProviderId, string> = { claude: '组长(Claude)', codex: '组长(Codex)' }
+const FALLBACK_MODELS: Record<ProviderId, string> = { claude: 'sonnet', codex: 'gpt-6.1-sol' }
+
+export const LEAD_SYSTEM_PROMPT = `# 组长的协作规范
+你负责理解成员的需求、拆分工作、分派给组员并汇总结果。你的权限是只读:可以查看共享目录中的文件,但不自己动手完成本该由组员完成的工作。
+
+## 什么时候开任务
+- 闲聊、简单问题、对已有结果的解释:直接回答,不开任务。
+- 需要组员动手(读写文件、分析数据、生成文档等):先 job_start(title, plan),plan 写清楚怎么拆分、交给谁。
+
+## 选组员
+- 根据能力卡片选组员。现有组员都不合适(或还没有组员)时,用 team_bot_catalog 查看可加入的机器人,再用 team_request_bot 申请,由成员审批;不要因为没有组员就自己动手。
+
+## 分派子任务
+- 一个子任务交给一个组员,目标单一、可以验收。
+- 说明要写清楚:输入(文件路径)、要做什么、产出放在 outputs/ 下的哪个路径、验收标准。组员看不到你和成员的对话。
+- 后续子任务需要前面的结果时用 depends_on,不要让组员自己去找。互不依赖的子任务会并行执行。
+- 分派之后简短告诉成员你的安排,然后结束本轮。
+
+## 收到结果
+- 还有子任务在进行、没有需要处理的事情:简短回复后结束本轮。
+- 子任务失败时先看原因:说明不清楚就改写说明后 task_retry;组员不擅长就换人(task_create);确实做不到就调整计划或 job_fail。同一个子任务不要重试超过两次。
+- 对结果有疑问时可以读取产出文件核对,但不要替组员修改。
+
+## 交付
+- 所有子任务都结束后调用 job_complete(summary, files, notes):
+  - summary 写给成员看:结论在前,说明每个交付文件是什么
+  - files 列出 outputs/ 下的交付文件
+  - notes 写给以后的自己:成员的偏好和要求、重要的决定、文件位置、遗留问题
+- 无法完成时调用 job_fail(reason, notes),说明原因和已经完成的部分。
+
+需要更详细的做法(子任务说明模板、验收清单、纪要怎么写)时,使用 ${LEAD_SKILL} 技能。`
+
+const LEAD_SKILL_DESCRIPTION = '工作组组长的详细做法:子任务说明模板、拆分原则、验收清单、任务纪要格式。只在你是工作组的组长时使用'
+
+export const LEAD_SKILL_MD = `---
+name: ${LEAD_SKILL}
+description: ${LEAD_SKILL_DESCRIPTION}
+---
+
+# 组长的详细做法
+
+## 拆分原则
+- 按"谁擅长"拆,而不是按步骤拆得越细越好:一个组员能一次做完的工作不要拆成多个子任务。
+- 有先后关系的才用 depends_on;可以同时做的尽量并行。
+- 每个子任务都要有明确的产出(文件或结论),否则无法验收。
+- 子任务数量受限额约束(通常 20 个以内),超出时任务会暂停。
+
+## 子任务说明模板
+\`\`\`
+目标:<一句话说明要完成什么>
+输入:<文件路径,如 uploads/q3.xlsx;或"见依赖的子任务 #1 的结果">
+要求:
+- <具体要求 1>
+- <具体要求 2>
+产出:<outputs/ 下的路径和格式,如 outputs/q3-summary.csv>
+验收标准:<怎样算完成,如"包含每个区域的销售额合计,数字与原表一致">
+\`\`\`
+
+## 验收清单
+收到子任务结果时检查:
+- 组员说的产出文件是否存在(可以用 Glob / Read 查看)
+- 内容是否满足验收标准;关键数字是否前后一致
+- 后续子任务需要的信息是否都在结果里
+不满足时用 task_retry 并在新说明中写清楚哪里不对。
+
+## 交付给成员
+- summary 结论在前;数字给出具体值;每个交付文件一句话说明用途。
+- 只列 outputs/ 下的最终文件,不列中间文件(.work/ 下的)。
+
+## 任务纪要(job_complete / job_fail 的 notes)
+写给以后的自己,几行即可:
+- 成员的偏好和明确要求(如"报告用中文,图表用柱状图")
+- 重要的决定及原因
+- 关键文件的位置和含义
+- 遗留问题或下次可以改进的地方
+系统会把任务的计划、子任务、结果和这段纪要一起保存到共享目录的 .team/jobs/ 下。以后遇到相关的工作时,先读对应的纪要。
+`
+
+/** 补齐每个引擎的组长 Profile;第一次创建时一起创建 team-lead 技能,并把使用通用助手的组长机器人改为组长 Profile */
+export function ensureLeadProfiles(deps: { profiles: ProfileService; customSkills: CustomSkillService; bots: BotService; settings: SettingsService }) {
+  const missing = (['claude', 'codex'] as const).filter(p => !deps.profiles.findBuiltinLead(p))
+  if (!missing.length) return
+  ensureLeadSkill(deps.customSkills)
+  const s = deps.settings.get()
+  for (const provider of missing) {
+    const lead = deps.profiles.create({
+      name: LEAD_NAMES[provider],
+      description: '工作组的组长:理解需求、拆分任务、分派给组员并汇总交付(只读)',
+      provider,
+      model: s.providers[provider].defaultModel || FALLBACK_MODELS[provider],
+      systemPrompt: LEAD_SYSTEM_PROMPT,
+      permissionMode: 'readonly',
+      allowedTools: [],
+      disallowedTools: [],
+      skills: [skillId(LEAD_SKILL)],
+      fileAccess: { mode: s.security.defaultFileAccessMode, read: [], write: [], deny: [] },
+      knowledgeRootIds: [],
+      isDefault: false,
+    }, true, 'lead')
+    const general = deps.profiles.getBuiltin(provider)
+    for (const bot of deps.bots.list()) {
+      if (bot.kind !== 'lead' || bot.profileId !== general.id || general.provider !== provider) continue
+      const { id: _id, createdAt: _c, updatedAt: _u, ...input } = bot
+      deps.bots.update(bot.id, { ...input, profileId: lead.id })
+    }
+  }
+}
+
+/** 已有同名技能时不覆盖 */
+function ensureLeadSkill(customSkills: CustomSkillService) {
+  if (customSkills.list().some(x => x.name === LEAD_SKILL)) return
+  const created = customSkills.create({ name: LEAD_SKILL, description: LEAD_SKILL_DESCRIPTION })
+  customSkills.writeFile(created.id, 'SKILL.md', Buffer.from(LEAD_SKILL_MD))
+  customSkills.save(created.id, { baseVersion: created.latestVersion, publish: true })
+}

+ 51 - 17
apps/server/src/teams/prompt.ts

@@ -5,8 +5,8 @@ import type { JobRow, TaskRow } from './jobs'
 
 const ROLE = { lead: '组长', worker: '组员' } as const
 
-/** 追加在 Profile 提示词之后:身份、工作组成员、协作约定。组长另外带上组员的能力卡片 */
-export function teamPrompt(opts: { title: string; human: TeamHuman; self: BotDto; bots: BotDto[]; cards?: BotCard[] }): string {
+/** 追加在 Profile 提示词之后:身份、工作组成员、协作机制。组长另外带上组员的能力卡片和最近的任务纪要 */
+export function teamPrompt(opts: { title: string; human: TeamHuman; self: BotDto; bots: BotDto[]; cards?: BotCard[]; memos?: JobRow[] }): string {
   const { self } = opts
   const others = opts.bots.filter(b => b.id !== self.id)
   const roster = others.length
@@ -23,28 +23,28 @@ ${roster}
   - uploads/ 是成员上传的文件
   - 交付的文件保存到 outputs/ 下,会以文件卡片显示在你的回复中
   - 你的中间文件放在 .work/${self.name}/ 下;不要修改或删除其他机器人的文件
+  - ${MEMO_DIR}/ 下是系统保存的任务纪要,只读
 - 在回复中写"@某个机器人"不会通知对方。`
-  return self.kind === 'lead' ? `${base}\n\n${leadRules(opts.cards ?? [])}` : `${base}\n\n${WORKER_RULES}`
+  return self.kind === 'lead' ? `${base}\n\n${leadMechanics(opts.cards ?? [], opts.memos ?? [])}` : `${base}\n\n${WORKER_RULES}`
 }
 
-function leadRules(cards: BotCard[]): string {
-  return `# 组长的工作方式
-你负责理解成员的需求、拆分工作、分派给组员并汇总结果。协作工具由 gateway 提供:
-- 闲聊、简单问题直接回答,不开任务。
-- 需要组员配合的复杂工作:先调用 job_start(title, plan) 开始一项任务(同一时间只能有一项),再用 task_create(bot_id, title, instructions, depends_on) 把子任务分派给组员。
-  - 说明要写清楚输入文件、期望的产出(文件放在 outputs/ 下的哪里)和验收标准,组员看不到你和成员的对话。
-  - 互不依赖的子任务会并行执行;depends_on 填写需要先完成的子任务编号,组员会收到它们的结果。
-- 分派之后结束本轮回复,简短告诉成员你的安排。每个子任务结束时你都会被唤醒,输入的最后附有当前任务的进度:
-  - 还有子任务在进行、没有需要处理的事情时,简短回复并结束本轮
-  - 子任务失败时,用 task_retry(task_no, instructions) 重试(可以改写说明,依赖它的子任务会继续),或者分派给其他组员、调整计划
-- 所有子任务都结束后,调用 job_complete(summary, files) 交付:summary 是给成员看的总结,files 是交付文件(outputs/ 下的路径)。无法完成时调用 job_fail(reason)。之后的最终回复简短即可。
+/** 组长的协作机制:与协作工具和调度方式绑定,由服务端生成。怎么拆分、验收和交付等协作规范在组长的 Profile 中 */
+function leadMechanics(cards: BotCard[], memos: JobRow[]): string {
+  const recent = memos.length
+    ? `\n\n# 最近的任务纪要\n${memos.map(j => `- ${memoPath(j)}:#${j.no}「${j.title}」${JOB_STATUS[j.status as keyof typeof JOB_STATUS] ?? j.status}`).join('\n')}\n遇到相关的工作时先读对应的纪要。`
+    : ''
+  return `# 组长的协作机制
+你是组长,协作工具(job_*、task_*、team_*)由 gateway 提供,用法见各工具的说明。系统的运行方式:
+- 同一时间只能有一项任务(job);子任务(task)只能在任务进行中分派。互不依赖的子任务并行执行,depends_on 中的子任务完成后才开始,组员会收到它们的结果。
+- 组员看不到你和成员的对话,只能看到子任务的说明和依赖子任务的结果。
+- 分派之后结束本轮即可。每个子任务结束时你都会被唤醒,输入的最后附有当前任务的进度。
 - 任务进行中成员发来的消息,是对当前任务的补充说明。
-- 现有组员都不适合(或还没有组员)时,用 team_bot_catalog() 查看可加入的机器人,再用 team_request_bot(bot_id, reason) 申请,由成员审批;不要因为没有组员就自己动手。
 - 超出限额时任务会暂停,等待成员决定,你不需要处理。
-- 不要自己动手完成本该由组员完成的工作。
+- 任务结束(job_complete / job_fail)时,系统把计划、子任务、结果和你填写的 notes 保存为任务纪要(${MEMO_DIR}/ 下)。
+- 拆分、分派、验收和交付的具体做法,按你的协作规范进行。
 
 # 当前组员的能力卡片
-${cards.length ? cards.map(cardText).join('\n\n') : '(还没有组员。可以用 team_bot_catalog() 查看可加入的机器人,用 team_request_bot 申请)'}`
+${cards.length ? cards.map(cardText).join('\n\n') : '(还没有组员。可以用 team_bot_catalog() 查看可加入的机器人,用 team_request_bot 申请)'}${recent}`
 }
 
 const WORKER_RULES = `# 子任务
@@ -97,6 +97,40 @@ function senderLabel(m: TeamMessage, selfId: string): string {
 }
 
 const TASK_EVENT = { completed: '已完成', failed: '失败', cancelled: '已取消' } as const
+const JOB_STATUS = { running: '进行中', blocked: '已暂停', completed: '已完成', failed: '失败', cancelled: '已取消' } as const
+const TASK_STATUS = { waiting: '等待依赖', queued: '排队中', running: '执行中', stub: '等待替身结果', completed: '已完成', failed: '失败', cancelled: '已取消' } as const
+
+/** 任务纪要:任务结束时保存到共享目录,组长以后可以读取(docs/design/13-bot-teams.md §17) */
+export const MEMO_DIR = '.team/jobs'
+export const memoPath = (job: Pick<JobRow, 'no'>) => `${MEMO_DIR}/job-${job.no}.md`
+
+export function jobMemo(job: JobRow, tasks: TaskRow[], notes: string, botName: (id: string) => string): string {
+  const time = (ts: string | null) => (ts ? new Date(ts).toLocaleString('zh-CN', { hour12: false }) : '—')
+  const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n)}…` : s)
+  const lines = [
+    `# 任务 #${job.no}:${job.title}`,
+    '',
+    `- 状态:${JOB_STATUS[job.status]}`,
+    `- 时间:${time(job.createdAt)} — ${time(job.endedAt)}`,
+    `- 组长执行 ${job.leadRuns} 轮,${tasks.length} 个子任务`,
+    '',
+    '## 计划',
+    job.plan.trim() || '(没有填写)',
+    '',
+    '## 子任务',
+  ]
+  if (!tasks.length) lines.push('(没有子任务)')
+  for (const t of tasks) {
+    lines.push('', `### #${t.no} ${botName(t.botId)} · ${t.title} · ${TASK_STATUS[t.status]}${t.dependsOn.length ? `(依赖 ${t.dependsOn.map(n => `#${n}`).join('、')})` : ''}`)
+    lines.push('', `说明:${clip(t.instructions.trim(), 1500)}`)
+    if (t.result) lines.push('', `结果:${clip(t.result.trim(), 2000)}`)
+    if (t.files.length) lines.push('', `文件:${t.files.map(f => f.path).join('、')}`)
+  }
+  lines.push('', job.status === 'completed' ? '## 交付' : '## 结束原因', job.summary?.trim() || '(没有填写)')
+  if (job.files.length) lines.push('', `交付文件:${job.files.map(f => f.path).join('、')}`)
+  lines.push('', '## 组长纪要', notes.trim() || '(没有填写)', '')
+  return lines.join('\n')
+}
 
 function clock(ts: string) {
   return new Date(ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })

+ 23 - 4
apps/server/src/teams/service.ts

@@ -10,6 +10,7 @@ import type {
   TeamMessage, TeamStatus, TeamStepDto, TeamUsageDto, TeamUsageRow,
 } from '@agent-server/shared'
 import type { Readable } from 'node:stream'
+import fs from 'node:fs'
 import path from 'node:path'
 import type { DB } from '../db'
 import { runs, sessions, teamBotSessions, teamBots, teams } from '../db/schema'
@@ -27,11 +28,14 @@ import type { BotKeyService, BotService } from './bots'
 import { cardText, type CardService } from './cards'
 import { TeamEventStore } from './events'
 import { JobStore, LIMIT_LABELS, OPEN_TASK, type JobRow, type TaskRow } from './jobs'
-import { taskInput, teamInput, teamPrompt } from './prompt'
+import { jobMemo, memoPath, taskInput, teamInput, teamPrompt } from './prompt'
 import { collabTools, LEAD_TOOLS, WORKER_TOOLS, type CollabArgs, type CollabToolName } from './tools'
 
 export type TeamRow = typeof teams.$inferSelect
 
+/** 组长的提示词中列出的最近任务纪要数 */
+const MEMO_LIMIT = 5
+
 const limitsPatch = teamLimitsSchema.partial()
 const debugPatch = z.object({ stepMode: z.boolean(), stubBotIds: z.array(z.string()).max(50) }).partial()
 
@@ -502,7 +506,7 @@ export class TeamService {
         const open = done ? this.jobs.tasks(team.id, { jobId: job.id, status: OPEN_TASK }) : []
         if (open.length) throw new Error(`还有未结束的子任务:${open.map(t => `#${t.no}(${TASK_STATUS[t.status]})`).join('、')}。请等它们完成,或先用 task_cancel 取消`)
         const files = done ? await this.resolvePaths(team, (args.files as string[] | undefined) ?? []) : []
-        this.endJob(job, done ? 'completed' : 'failed', String(done ? args.summary : args.reason), files, ref.botId)
+        this.endJob(job, done ? 'completed' : 'failed', String(done ? args.summary : args.reason), files, ref.botId, String(args.notes ?? ''))
         rt.posted = true
         return done ? '任务已完成并交付给成员。' : '已告知成员任务失败。'
       }
@@ -675,7 +679,8 @@ export class TeamService {
     const profile = this.deps.profiles.get(bot.profileId)
     const members = this.memberBots(team.id)
     const cards = member.role === 'lead' ? await this.deps.cards.cards(members.filter(b => this.roleOf(team.id, b.id) === 'worker')) : undefined
-    const prompt = teamPrompt({ title: team.title, human: team.human, self: bot, bots: members, cards })
+    const memos = member.role === 'lead' ? this.jobs.jobs(team.id).filter(j => j.endedAt).slice(0, MEMO_LIMIT) : undefined
+    const prompt = teamPrompt({ title: team.title, human: team.human, self: bot, bots: members, cards, memos })
     const title = `${team.title} · ${bot.name}`
 
     let session: SessionRow | null = null
@@ -878,10 +883,11 @@ export class TeamService {
     }
   }
 
-  private endJob(job: JobRow, status: 'completed' | 'failed' | 'cancelled', summary: string, files: TeamFileRef[] = [], leadId?: string) {
+  private endJob(job: JobRow, status: 'completed' | 'failed' | 'cancelled', summary: string, files: TeamFileRef[] = [], leadId?: string, notes = '') {
     for (const t of this.jobs.tasks(job.teamId, { jobId: job.id, status: OPEN_TASK })) this.cancelTask(t)
     const next = this.jobs.updateJob(job.id, this.jobs.end(status, { summary, files }))
     this.emitJob(next)
+    this.writeMemo(next, notes)
     const card: TeamCard = { kind: 'job', jobId: job.id, event: status }
     const head = { completed: '已完成', failed: '失败', cancelled: '已取消' }[status]
     const text = `任务「${job.title}」${head}\n\n${summary}`
@@ -890,6 +896,19 @@ export class TeamService {
     this.changed(job.teamId)
   }
 
+  /** 任务纪要写到共享目录(.team/jobs/job-<no>.md),组长以后可以读取;写入失败只记日志 */
+  private writeMemo(job: JobRow, notes: string) {
+    const team = this.findRow(job.teamId)
+    if (!team) return
+    const file = path.join(team.workspaceDir, memoPath(job))
+    try {
+      fs.mkdirSync(path.dirname(file), { recursive: true })
+      fs.writeFileSync(file, jobMemo(job, this.jobs.tasks(job.teamId, { jobId: job.id }), notes, id => this.botName(id)))
+    } catch (err) {
+      this.deps.log.warn({ err, teamId: job.teamId, jobId: job.id }, '任务纪要写入失败')
+    }
+  }
+
   /** 交给组长的任务进度:每个子任务的状态,以及因依赖失败而无法开始的子任务 */
   private progressText(job: JobRow): string {
     const list = this.jobs.tasks(job.teamId, { jobId: job.id })

+ 3 - 2
apps/server/src/teams/tools.ts

@@ -5,6 +5,7 @@ import { z } from 'zod'
 import { fail, ok, type BuiltinTool, type ToolResult } from '../mcp-gateway/builtin'
 import type { GatewayRunContext } from '../mcp-gateway/tokens'
 
+const notesArg = z.string().max(10_000).optional().describe('任务纪要:写给以后的自己(成员的偏好、重要的决定、文件位置、遗留问题),与任务记录一起保存')
 const filesArg = z.array(z.string()).max(50).optional().describe('交付文件:共享目录中 outputs/ 下的相对路径')
 
 const schemas = {
@@ -16,8 +17,8 @@ const schemas = {
     limit: z.number().int().min(1).max(100).optional().describe('条数,默认 30'),
   }),
   job_start: z.object({ title: z.string().min(1).max(100).describe('任务标题'), plan: z.string().max(10_000).describe('计划:打算怎么拆分、交给谁') }),
-  job_complete: z.object({ summary: z.string().min(1).max(50_000).describe('交给成员的总结'), files: filesArg }),
-  job_fail: z.object({ reason: z.string().min(1).max(10_000) }),
+  job_complete: z.object({ summary: z.string().min(1).max(50_000).describe('交给成员的总结'), files: filesArg, notes: notesArg }),
+  job_fail: z.object({ reason: z.string().min(1).max(10_000), notes: notesArg }),
   task_create: z.object({
     bot_id: z.string().describe('组员的机器人 id(见能力卡片)'),
     title: z.string().min(1).max(100),

+ 3 - 2
apps/server/test/custom-skills.test.ts

@@ -51,9 +51,10 @@ describe('自定义技能:目录技能源导入与同步', () => {
     expect(r.imported.map(x => [x.name, x.version, x.created])).toEqual([['alpha', 1, true], ['beta', 1, true]])
     expect(fs.readFileSync(live('alpha'), 'utf8')).toContain('name: alpha')
     expect(fs.existsSync(live('beta', 'passwd'))).toBe(false)
-    expect(app.customSkills.list().map(x => [x.name, x.origin, x.publishedVersion, x.sourceName])).toEqual([['alpha', 'imported', 1, '业务仓库'], ['beta', 'imported', 1, '业务仓库']])
+    // 内置的 team-lead 技能(组长 Profile 使用)随服务创建
+    expect(app.customSkills.list().filter(x => x.name !== 'team-lead').map(x => [x.name, x.origin, x.publishedVersion, x.sourceName])).toEqual([['alpha', 'imported', 1, '业务仓库'], ['beta', 'imported', 1, '业务仓库']])
     expect(app.skillSources.list()[0]!.skillNames).toEqual(['alpha', 'beta'])
-    expect(app.customSkills.pluginRef()!.skills.map(s => [s.id, s.optIn])).toEqual([['custom-skills:alpha', true], ['custom-skills:beta', true]])
+    expect(app.customSkills.pluginRef()!.skills.filter(s => s.id !== 'custom-skills:team-lead').map(s => [s.id, s.optIn])).toEqual([['custom-skills:alpha', true], ['custom-skills:beta', true]])
   })
 
   it('C-02 修改来源中的一个参考文件:同步只显示该文件的变化,导入后生效目录更新', () => {

+ 26 - 1
apps/server/test/team-jobs.test.ts

@@ -106,6 +106,19 @@ describe('能力卡片', () => {
   })
 })
 
+describe('内置的组长 Profile', () => {
+  it('每个引擎一个只读的组长 Profile,协作规范在系统提示词中,带 team-lead 技能;不影响通用助手', async () => {
+    const lead = app.profiles.findBuiltinLead('claude')!
+    expect(lead).toMatchObject({ builtin: true, builtinKey: 'lead', permissionMode: 'readonly', skills: ['custom-skills:team-lead'] })
+    expect(lead.systemPrompt).toContain('# 组长的协作规范')
+    expect(app.profiles.findBuiltinLead('codex')).toMatchObject({ provider: 'codex', builtinKey: 'lead' })
+    expect(app.profiles.getBuiltin('claude').builtinKey).toBeNull()
+    const skill = app.customSkills.list().find(x => x.name === 'team-lead')!
+    expect(skill).toMatchObject({ origin: 'authored', includeInAll: false, publishedVersion: 2 })
+    expect((await server.inject({ method: 'DELETE', url: `/admin/v1/profiles/${lead.id}`, cookies: adminCookie })).statusCode).toBe(400)
+  })
+})
+
 describe('任务流程', () => {
   it('只有组长:申请加入两个机器人(经审批),拆成有依赖的子任务,组员交付文件,组长汇总交付', async () => {
     const team = (await server.inject({ method: 'POST', url: '/bot/v1/teams', headers: bk(), payload: { human, title: '周报', leadBotId: ids['组长'] } })).json() as TeamDetailDto
@@ -127,7 +140,7 @@ describe('任务流程', () => {
         return '已安排:数据员统计,文档员写报告'
       }
       if (input.includes('子任务 #2 已完成')) {
-        const r = await call('job_complete', { summary: '周报已完成', files: ['outputs/report.md'] })
+        const r = await call('job_complete', { summary: '周报已完成', files: ['outputs/report.md'], notes: '成员要求周报用中文' })
         expect(r.isError).toBe(false)
         return '交付完毕'
       }
@@ -187,6 +200,18 @@ describe('任务流程', () => {
     expect(usage.jobs[0].runs).toBe(usage.total.runs)
     expect(usage.tasks).toHaveLength(2)
     expect(usage.bots.map((b: { botId: string }) => b.botId).sort()).toEqual([ids['组长'], ids['数据员'], ids['文档员']].sort())
+
+    // 任务纪要:计划、子任务、交付和组长的 notes 保存到共享目录,以后组长的提示词中列出
+    const memo = fs.readFileSync(path.join(app.teams.getRow(team.id).workspaceDir, '.team/jobs/job-1.md'), 'utf8')
+    expect(memo).toContain('# 任务 #1:周报')
+    expect(memo).toContain('先统计再写报告')
+    expect(memo).toContain('### #1 数据员 · 统计 · 已完成')
+    expect(memo).toContain('销量 100')
+    expect(memo).toContain('交付文件:outputs/report.md')
+    expect(memo).toContain('成员要求周报用中文')
+    await server.inject({ method: 'POST', url: `/bot/v1/teams/${team.id}/messages`, headers: bk(), payload: { text: '谢谢' } })
+    await waitFor(() => idle(team.id) && fake.inputs.at(-1)?.text.includes('谢谢'))
+    expect(fake.inputs.at(-1)!.prompt).toContain('- .team/jobs/job-1.md:#1「周报」已完成')
   })
 
   it('同一时间只有一个任务;组员不在子任务中时 task_complete 失败', async () => {

+ 19 - 3
docs/design/13-bot-teams.md

@@ -1,6 +1,6 @@
 # 13 Bot 工作组
 
-> 状态:**一期(2026-10-26)、二期与三期(2026-10-27)已实施**;实施中的调整见 §15、§16、§17。决策见 ADR-0023(工作组协作模型)、ADR-0024(Bot API 与独立的 Bot Key)。
+> 状态:**一期(2026-10-26)、二期与三期(2026-10-27)已实施**;实施中的调整见 §15–§18。决策见 ADR-0023(工作组协作模型)、ADR-0024(Bot API 与独立的 Bot Key)。
 
 ## 1. 背景与目标
 
@@ -355,9 +355,9 @@ SSE 事件:
 
 | # | 问题 | 倾向 |
 |---|---|---|
-| T1 | 组长的提示词和协作规范放在哪里 | 内置一个"组长"Profile 和配套技能,后台可以复制修改 |
+| T1 | 组长的提示词和协作规范放在哪里 | ✅ 内置"组长"Profile(每个引擎一个)和 team-lead 技能,后台可以修改或复制(§18) |
 | T2 | 组长能否申请移除组员 | 一期、二期不支持,由成员操作 |
-| T3 | 工作组长期运行后,组长的 Session 越来越长 | 依赖引擎自身的压缩;任务结束时由组长写一份任务纪要到共享目录,以后再观察是否需要每个任务重新开一个组长 Session |
+| T3 | 工作组长期运行后,组长的 Session 越来越长 | 任务纪要已实施(§18);何时为组长重开 Session 待定 |
 | T4 | 能力卡片里是否列出全部 MCP 工具 | 只列服务名和说明,工具较多时不展开 |
 
 ## 15. 一期实施记录(2026-10-26)
@@ -429,3 +429,19 @@ SSE 事件:
   - 只有组长的工作组:组长申请加入数据员和文档员(在 `<TeamChat>` 的审批卡片中批准),拆成有依赖的两个子任务,交付两个文件。
   - 子任务数上限为 1 时任务暂停,选择继续后完成。
   - 把这个工作组保存为回归用例并重放,结果一致。
+
+## 18. 组长 Profile 与任务纪要(2026-10-27)
+
+**组长 Profile**(T1):
+- 每个引擎一个内置的"组长"Profile(`profiles.builtinKey = 'lead'`),不能删除,可以修改,也可以复制后修改。
+- 权限为只读:组长可以查看共享目录中的文件,但不能写文件、不能执行命令,从机制上防止它自己动手。协作工具不受只读限制。
+- 系统提示词是"协作规范":什么时候开任务、怎么选组员、怎么写子任务说明、失败时怎么处理、怎么交付和写纪要。
+- 技能只启用自定义技能 `team-lead`:子任务说明模板、拆分原则、验收清单、纪要格式。它不包含在"全部技能"中,组员不会加载。
+- 服务端的提示词只保留与协作工具和调度方式绑定的"协作机制",加上组员的能力卡片和最近的任务纪要。
+- 第一次启动时创建这两个 Profile 和技能,并把使用"通用助手"的组长机器人改为同一引擎的组长 Profile。
+- 后台新建机器人时,类型选"组长"会自动选上组长 Profile;组长使用其他 Profile 时给出提示。
+
+**任务纪要**(T3 的第一步):
+- 任务结束(完成、失败、取消)时,系统把任务的计划、每个子任务的说明、结果和文件、交付内容,以及组长在 `job_complete` / `job_fail` 的 `notes` 中写的要点,保存到共享目录的 `.team/jobs/job-<编号>.md`。
+- 纪要由系统根据任务记录生成,不需要组长额外执行一轮;组长只需写几行"给以后的自己"的要点。
+- 组长的提示词中列出最近 5 份纪要的路径和标题,遇到相关工作时可以读取。

+ 3 - 1
packages/shared/src/api.ts

@@ -90,11 +90,13 @@ export interface ProfileDto {
   isDefault: boolean
   /** 内置 Profile(通用助手)不可删除 */
   builtin: boolean
+  /** 内置 Profile 的种类:null 为通用助手,lead 为工作组的组长 */
+  builtinKey: 'lead' | null
   version: number
   updatedAt: string
 }
 
-export type ProfileInput = Omit<ProfileDto, 'id' | 'version' | 'updatedAt' | 'builtin' | 'mcpServerIds'> & { mcpServerIds?: string[] }
+export type ProfileInput = Omit<ProfileDto, 'id' | 'version' | 'updatedAt' | 'builtin' | 'builtinKey' | 'mcpServerIds'> & { mcpServerIds?: string[] }
 
 /** Profile 历史版本的内容(不含"是否默认") */
 export type ProfileSnapshot = Omit<ProfileInput, 'isDefault'>