@humanclaw/humanclaw 1.2.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -72,6 +72,37 @@ function initSchema(db2) {
72
72
  key TEXT PRIMARY KEY,
73
73
  value TEXT NOT NULL
74
74
  );
75
+
76
+ CREATE TABLE IF NOT EXISTS teams (
77
+ team_id TEXT PRIMARY KEY,
78
+ name TEXT NOT NULL,
79
+ description TEXT NOT NULL DEFAULT '',
80
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS team_members (
84
+ team_id TEXT NOT NULL REFERENCES teams(team_id) ON DELETE CASCADE,
85
+ agent_id TEXT NOT NULL REFERENCES agents(agent_id) ON DELETE CASCADE,
86
+ relationship TEXT NOT NULL DEFAULT '',
87
+ PRIMARY KEY (team_id, agent_id)
88
+ );
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_team_members_agent ON team_members(agent_id);
91
+
92
+ CREATE TABLE IF NOT EXISTS evaluations (
93
+ eval_id TEXT PRIMARY KEY,
94
+ job_id TEXT NOT NULL REFERENCES jobs(job_id) ON DELETE CASCADE,
95
+ agent_id TEXT NOT NULL REFERENCES agents(agent_id) ON DELETE CASCADE,
96
+ trace_id TEXT NOT NULL REFERENCES tasks(trace_id) ON DELETE CASCADE,
97
+ rating_system TEXT NOT NULL CHECK (rating_system IN ('ali', 'letter', 'em')),
98
+ rating TEXT NOT NULL,
99
+ weight REAL NOT NULL DEFAULT 1.0,
100
+ comment TEXT NOT NULL DEFAULT '',
101
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
102
+ );
103
+
104
+ CREATE INDEX IF NOT EXISTS idx_evaluations_job ON evaluations(job_id);
105
+ CREATE INDEX IF NOT EXISTS idx_evaluations_agent ON evaluations(agent_id);
75
106
  `);
76
107
  const cols = db2.prepare("PRAGMA table_info(agents)").all();
77
108
  if (!cols.some((c) => c.name === "relationship")) {
@@ -125,6 +156,19 @@ function deleteAgent(agentId, db2) {
125
156
  const result = conn.prepare("DELETE FROM agents WHERE agent_id = ?").run(agentId);
126
157
  return result.changes > 0;
127
158
  }
159
+ function getAgentWithTeams(agentId, db2) {
160
+ const conn = db2 ?? getDb();
161
+ const agent = getAgent(agentId, conn);
162
+ if (!agent) return void 0;
163
+ const teams = conn.prepare(
164
+ `SELECT t.team_id, t.name AS team_name, tm.relationship
165
+ FROM team_members tm
166
+ JOIN teams t ON tm.team_id = t.team_id
167
+ WHERE tm.agent_id = ?
168
+ ORDER BY t.created_at`
169
+ ).all(agentId);
170
+ return { ...agent, teams };
171
+ }
128
172
  function listAgentsWithMetrics(db2) {
129
173
  const conn = db2 ?? getDb();
130
174
  const rows = conn.prepare(
@@ -203,6 +247,14 @@ router.patch("/:agent_id/status", (req, res) => {
203
247
  }
204
248
  res.json({ agent_id, status });
205
249
  });
250
+ router.get("/:agent_id", (req, res) => {
251
+ const agent = getAgentWithTeams(req.params.agent_id);
252
+ if (!agent) {
253
+ res.status(404).json({ error: `Agent not found: ${req.params.agent_id}` });
254
+ return;
255
+ }
256
+ res.json(agent);
257
+ });
206
258
  router.delete("/:agent_id", (req, res) => {
207
259
  const { agent_id } = req.params;
208
260
  const deleted = deleteAgent(agent_id);
@@ -378,6 +430,70 @@ function dispatchJob(request, db2) {
378
430
  return { ...job, tasks };
379
431
  }
380
432
 
433
+ // src/models/team.ts
434
+ function createTeam(team, db2) {
435
+ const conn = db2 ?? getDb();
436
+ const now = (/* @__PURE__ */ new Date()).toISOString();
437
+ conn.prepare(
438
+ `INSERT INTO teams (team_id, name, description, created_at)
439
+ VALUES (?, ?, ?, ?)`
440
+ ).run(team.team_id, team.name, team.description || "", now);
441
+ return { ...team, description: team.description || "", created_at: now };
442
+ }
443
+ function getTeam(teamId, db2) {
444
+ const conn = db2 ?? getDb();
445
+ const row = conn.prepare("SELECT * FROM teams WHERE team_id = ?").get(teamId);
446
+ return row || void 0;
447
+ }
448
+ function listTeams(db2) {
449
+ const conn = db2 ?? getDb();
450
+ return conn.prepare("SELECT * FROM teams ORDER BY created_at").all();
451
+ }
452
+ function deleteTeam(teamId, db2) {
453
+ const conn = db2 ?? getDb();
454
+ const result = conn.prepare("DELETE FROM teams WHERE team_id = ?").run(teamId);
455
+ return result.changes > 0;
456
+ }
457
+ function addTeamMember(teamId, agentId, relationship, db2) {
458
+ const conn = db2 ?? getDb();
459
+ conn.prepare(
460
+ `INSERT OR REPLACE INTO team_members (team_id, agent_id, relationship)
461
+ VALUES (?, ?, ?)`
462
+ ).run(teamId, agentId, relationship || "");
463
+ }
464
+ function removeTeamMember(teamId, agentId, db2) {
465
+ const conn = db2 ?? getDb();
466
+ const result = conn.prepare("DELETE FROM team_members WHERE team_id = ? AND agent_id = ?").run(teamId, agentId);
467
+ return result.changes > 0;
468
+ }
469
+ function updateTeamMemberRelationship(teamId, agentId, relationship, db2) {
470
+ const conn = db2 ?? getDb();
471
+ const result = conn.prepare(
472
+ "UPDATE team_members SET relationship = ? WHERE team_id = ? AND agent_id = ?"
473
+ ).run(relationship, teamId, agentId);
474
+ return result.changes > 0;
475
+ }
476
+ function getTeamWithMembers(teamId, db2) {
477
+ const conn = db2 ?? getDb();
478
+ const team = getTeam(teamId, conn);
479
+ if (!team) return void 0;
480
+ const members = conn.prepare(
481
+ `SELECT tm.team_id, tm.agent_id, tm.relationship, a.name AS agent_name, a.status AS agent_status
482
+ FROM team_members tm
483
+ JOIN agents a ON tm.agent_id = a.agent_id
484
+ WHERE tm.team_id = ?
485
+ ORDER BY a.created_at`
486
+ ).all(teamId);
487
+ return { ...team, members };
488
+ }
489
+ function getTeamMemberRelationship(teamId, agentId, db2) {
490
+ const conn = db2 ?? getDb();
491
+ const row = conn.prepare(
492
+ "SELECT relationship FROM team_members WHERE team_id = ? AND agent_id = ?"
493
+ ).get(teamId, agentId);
494
+ return row?.relationship;
495
+ }
496
+
381
497
  // src/llm/claude.ts
382
498
  var DEFAULT_BASE_URL = "https://api.anthropic.com";
383
499
  var ClaudeProvider = class {
@@ -466,6 +582,62 @@ var OpenAIProvider = class {
466
582
  }
467
583
  };
468
584
 
585
+ // src/llm/responses.ts
586
+ var DEFAULT_BASE_URL3 = "https://api.openai.com";
587
+ var ResponsesProvider = class {
588
+ apiKey;
589
+ model;
590
+ baseUrl;
591
+ constructor(apiKey, model, baseUrl) {
592
+ this.apiKey = apiKey;
593
+ this.model = model || "gpt-4o";
594
+ this.baseUrl = (baseUrl || DEFAULT_BASE_URL3).replace(/\/+$/, "");
595
+ }
596
+ async complete(request) {
597
+ const systemMessages = request.messages.filter((m) => m.role === "system");
598
+ const nonSystemMessages = request.messages.filter((m) => m.role !== "system");
599
+ const input = nonSystemMessages.map((m) => ({
600
+ role: m.role,
601
+ content: m.content
602
+ }));
603
+ const body = {
604
+ model: this.model,
605
+ input
606
+ };
607
+ if (systemMessages.length > 0) {
608
+ body.instructions = systemMessages.map((m) => m.content).join("\n\n");
609
+ }
610
+ if (request.temperature !== void 0) {
611
+ body.temperature = request.temperature;
612
+ }
613
+ if (request.max_tokens) {
614
+ body.max_output_tokens = request.max_tokens;
615
+ }
616
+ const response = await fetch(`${this.baseUrl}/v1/responses`, {
617
+ method: "POST",
618
+ headers: {
619
+ "Content-Type": "application/json",
620
+ "Authorization": `Bearer ${this.apiKey}`
621
+ },
622
+ body: JSON.stringify(body)
623
+ });
624
+ if (!response.ok) {
625
+ const errorText = await response.text();
626
+ throw new Error(`Responses API error (${response.status}): ${errorText}`);
627
+ }
628
+ const data = await response.json();
629
+ for (const item of data.output) {
630
+ if (item.type === "message" && item.content) {
631
+ const textBlock = item.content.find((c) => c.type === "output_text");
632
+ if (textBlock) {
633
+ return { content: textBlock.text };
634
+ }
635
+ }
636
+ }
637
+ throw new Error("Responses API returned no text content");
638
+ }
639
+ };
640
+
469
641
  // src/models/config.ts
470
642
  function getConfig(key, db2) {
471
643
  const conn = db2 ?? getDb();
@@ -482,12 +654,17 @@ function deleteConfig(key, db2) {
482
654
  }
483
655
 
484
656
  // src/llm/index.ts
657
+ function normalizeProvider(raw) {
658
+ if (raw === "claude") return "anthropic";
659
+ return raw;
660
+ }
485
661
  function getLlmConfig() {
486
662
  const dbProvider = getConfig("llm_provider");
487
663
  const dbApiKey = getConfig("llm_api_key");
488
664
  const dbModel = getConfig("llm_model");
489
665
  const dbBaseUrl = getConfig("llm_base_url");
490
- const provider = dbProvider || process.env.HUMANCLAW_LLM_PROVIDER || "claude";
666
+ const rawProvider = dbProvider || process.env.HUMANCLAW_LLM_PROVIDER || "anthropic";
667
+ const provider = normalizeProvider(rawProvider);
491
668
  const apiKey = dbApiKey || process.env.HUMANCLAW_LLM_API_KEY || "";
492
669
  const model = dbModel || process.env.HUMANCLAW_LLM_MODEL;
493
670
  const baseUrl = dbBaseUrl || process.env.HUMANCLAW_LLM_BASE_URL;
@@ -496,18 +673,21 @@ function getLlmConfig() {
496
673
  "\u672A\u914D\u7F6E LLM API Key\u3002\u8BF7\u5728 Dashboard \u8BBE\u7F6E\u4E2D\u914D\u7F6E\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF HUMANCLAW_LLM_API_KEY\u3002"
497
674
  );
498
675
  }
499
- if (provider !== "claude" && provider !== "openai") {
500
- throw new Error(`\u4E0D\u652F\u6301\u7684 LLM \u63D0\u4F9B\u5546: ${provider}\u3002\u652F\u6301: claude, openai`);
676
+ const validFormats = ["openai", "anthropic", "responses"];
677
+ if (!validFormats.includes(provider)) {
678
+ throw new Error(`\u4E0D\u652F\u6301\u7684 API \u683C\u5F0F: ${provider}\u3002\u652F\u6301: openai, anthropic, responses`);
501
679
  }
502
680
  return { provider, apiKey, model: model || void 0, baseUrl: baseUrl || void 0 };
503
681
  }
504
682
  function createLlmProvider(config) {
505
683
  const cfg = config || getLlmConfig();
506
684
  switch (cfg.provider) {
507
- case "claude":
685
+ case "anthropic":
508
686
  return new ClaudeProvider(cfg.apiKey, cfg.model, cfg.baseUrl);
509
687
  case "openai":
510
688
  return new OpenAIProvider(cfg.apiKey, cfg.model, cfg.baseUrl);
689
+ case "responses":
690
+ return new ResponsesProvider(cfg.apiKey, cfg.model, cfg.baseUrl);
511
691
  }
512
692
  }
513
693
 
@@ -536,16 +716,23 @@ function buildSystemPrompt() {
536
716
  ]
537
717
  \`\`\``;
538
718
  }
539
- function buildUserPrompt(prompt, agents) {
719
+ function buildUserPrompt(prompt, agents, teamContext) {
540
720
  const now = /* @__PURE__ */ new Date();
541
721
  const agentList = agents.map((a) => {
542
722
  const load = a.active_task_count > 0 ? `\u5F53\u524D\u6709 ${a.active_task_count} \u4E2A\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1` : "\u5F53\u524D\u7A7A\u95F2";
543
723
  const speed = a.avg_delivery_hours !== null ? `\u5E73\u5747\u4EA4\u4ED8\u65F6\u95F4 ${a.avg_delivery_hours}h` : "\u6682\u65E0\u5386\u53F2\u6570\u636E";
544
- const rel = a.relationship ? ` \u5173\u7CFB: ${a.relationship}` : "";
724
+ const relText = teamContext?.relationships.get(a.agent_id) || a.relationship;
725
+ const rel = relText ? ` \u5173\u7CFB: ${relText}` : "";
545
726
  return `- ${a.name} (ID: ${a.agent_id}) \u6280\u80FD: [${a.capabilities.join(", ")}]${rel} ${load} ${speed}`;
546
727
  }).join("\n");
728
+ let teamInfo = "";
729
+ if (teamContext) {
730
+ teamInfo = `
731
+ \u56E2\u961F: ${teamContext.name}${teamContext.description ? ` \u2014 ${teamContext.description}` : ""}
732
+ `;
733
+ }
547
734
  return `\u5F53\u524D\u65F6\u95F4: ${now.toISOString()}
548
-
735
+ ${teamInfo}
549
736
  \u53EF\u7528\u7684\u78B3\u57FA\u8282\u70B9\uFF1A
550
737
  ${agentList}
551
738
 
@@ -605,7 +792,25 @@ async function planJob(request, provider, db2) {
605
792
  const conn = db2 ?? getDb();
606
793
  const allAgents = listAgentsWithMetrics(conn);
607
794
  let agents;
608
- if (request.agent_ids && request.agent_ids.length > 0) {
795
+ let teamContext;
796
+ if (request.team_id) {
797
+ const team = getTeamWithMembers(request.team_id, conn);
798
+ if (!team) {
799
+ throw new Error(`\u56E2\u961F\u4E0D\u5B58\u5728: ${request.team_id}`);
800
+ }
801
+ if (team.members.length === 0) {
802
+ throw new Error(`\u56E2\u961F "${team.name}" \u4E2D\u6CA1\u6709\u6210\u5458`);
803
+ }
804
+ const memberIds = new Set(team.members.map((m) => m.agent_id));
805
+ agents = allAgents.filter((a) => memberIds.has(a.agent_id));
806
+ const relationships = /* @__PURE__ */ new Map();
807
+ for (const m of team.members) {
808
+ if (m.relationship) {
809
+ relationships.set(m.agent_id, m.relationship);
810
+ }
811
+ }
812
+ teamContext = { name: team.name, description: team.description, relationships };
813
+ } else if (request.agent_ids && request.agent_ids.length > 0) {
609
814
  agents = allAgents.filter((a) => request.agent_ids.includes(a.agent_id));
610
815
  if (agents.length === 0) {
611
816
  throw new Error("\u6307\u5B9A\u7684 Agent \u5747\u4E0D\u5B58\u5728");
@@ -621,7 +826,7 @@ async function planJob(request, provider, db2) {
621
826
  }
622
827
  const llm = provider ?? createLlmProvider();
623
828
  const systemPrompt = buildSystemPrompt();
624
- const userPrompt = buildUserPrompt(request.prompt, agents);
829
+ const userPrompt = buildUserPrompt(request.prompt, agents, teamContext);
625
830
  const response = await llm.complete({
626
831
  messages: [
627
832
  { role: "system", content: systemPrompt },
@@ -762,7 +967,7 @@ ${taskDescription}
762
967
  4. \u5B57\u6570 200-400 \u5B57
763
968
  5. \u76F4\u63A5\u8F93\u51FA\u6C47\u62A5\u5185\u5BB9\uFF0C\u4E0D\u8981\u52A0\u4EFB\u4F55\u683C\u5F0F\u524D\u7F00\u6216\u8BF4\u660E`;
764
969
  }
765
- async function simulateDelivery(traceId, provider, db2) {
970
+ async function simulateDelivery(traceId, provider, db2, teamId) {
766
971
  const conn = db2 ?? getDb();
767
972
  const task = getTask(traceId, conn);
768
973
  if (!task) {
@@ -772,6 +977,11 @@ async function simulateDelivery(traceId, provider, db2) {
772
977
  if (!agent) {
773
978
  throw new Error(`Agent not found: ${task.assignee_id}`);
774
979
  }
980
+ let relationship = agent.relationship;
981
+ if (teamId) {
982
+ const teamRel = getTeamMemberRelationship(teamId, agent.agent_id, conn);
983
+ if (teamRel) relationship = teamRel;
984
+ }
775
985
  const llm = provider ?? createLlmProvider();
776
986
  const response = await llm.complete({
777
987
  messages: [
@@ -783,7 +993,7 @@ async function simulateDelivery(traceId, provider, db2) {
783
993
  role: "user",
784
994
  content: buildSimulatePrompt(
785
995
  agent.name,
786
- agent.relationship,
996
+ relationship,
787
997
  agent.capabilities,
788
998
  task.todo_description,
789
999
  task.deadline
@@ -838,13 +1048,13 @@ router3.post("/reject", (req, res) => {
838
1048
  }
839
1049
  });
840
1050
  router3.post("/simulate", async (req, res) => {
841
- const { trace_id } = req.body;
1051
+ const { trace_id, team_id } = req.body;
842
1052
  if (!trace_id) {
843
1053
  res.status(400).json({ error: "trace_id is required" });
844
1054
  return;
845
1055
  }
846
1056
  try {
847
- const result = await simulateDelivery(trace_id);
1057
+ const result = await simulateDelivery(trace_id, void 0, void 0, team_id);
848
1058
  res.json(result);
849
1059
  } catch (error) {
850
1060
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -857,9 +1067,72 @@ var tasks_default = router3;
857
1067
  // src/routes/sync.ts
858
1068
  import { Router as Router4 } from "express";
859
1069
 
1070
+ // src/models/evaluation.ts
1071
+ var RATING_SYSTEMS = {
1072
+ ali: ["3.25", "3.5", "3.5+", "3.75", "4.0"],
1073
+ letter: ["S", "A", "B", "C", "D"],
1074
+ em: ["EM+", "EM", "MM+", "MM", "MM-"]
1075
+ };
1076
+ function validateRating(system, rating) {
1077
+ const values = RATING_SYSTEMS[system];
1078
+ return values ? values.includes(rating) : false;
1079
+ }
1080
+ function createEvaluation(evaluation, db2) {
1081
+ const conn = db2 ?? getDb();
1082
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1083
+ conn.prepare(
1084
+ `INSERT INTO evaluations (eval_id, job_id, agent_id, trace_id, rating_system, rating, weight, comment, created_at)
1085
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1086
+ ).run(
1087
+ evaluation.eval_id,
1088
+ evaluation.job_id,
1089
+ evaluation.agent_id,
1090
+ evaluation.trace_id,
1091
+ evaluation.rating_system,
1092
+ evaluation.rating,
1093
+ evaluation.weight,
1094
+ evaluation.comment || "",
1095
+ now
1096
+ );
1097
+ return { ...evaluation, comment: evaluation.comment || "", created_at: now };
1098
+ }
1099
+ function rowToEvaluation(row) {
1100
+ return {
1101
+ ...row,
1102
+ rating_system: row.rating_system
1103
+ };
1104
+ }
1105
+ function listEvaluationsByJob(jobId, db2) {
1106
+ const conn = db2 ?? getDb();
1107
+ const rows = conn.prepare(
1108
+ `SELECT e.*
1109
+ FROM evaluations e
1110
+ WHERE e.job_id = ?
1111
+ ORDER BY e.created_at`
1112
+ ).all(jobId);
1113
+ return rows.map(rowToEvaluation);
1114
+ }
1115
+ function listEvaluationsByAgent(agentId, db2) {
1116
+ const conn = db2 ?? getDb();
1117
+ const rows = conn.prepare(
1118
+ `SELECT e.*, j.original_prompt, t.todo_description
1119
+ FROM evaluations e
1120
+ LEFT JOIN jobs j ON e.job_id = j.job_id
1121
+ LEFT JOIN tasks t ON e.trace_id = t.trace_id
1122
+ WHERE e.agent_id = ?
1123
+ ORDER BY e.created_at DESC`
1124
+ ).all(agentId);
1125
+ return rows.map((r) => ({ ...rowToEvaluation(r), original_prompt: r.original_prompt, todo_description: r.todo_description }));
1126
+ }
1127
+ function deleteEvaluationsByJob(jobId, db2) {
1128
+ const conn = db2 ?? getDb();
1129
+ const result = conn.prepare("DELETE FROM evaluations WHERE job_id = ?").run(jobId);
1130
+ return result.changes;
1131
+ }
1132
+
860
1133
  // src/services/reviewer.ts
861
- function buildReviewSystemPrompt() {
862
- return `\u4F60\u662F HumanClaw \u4EA4\u4ED8\u5BA1\u67E5\u5458\u3002\u4F60\u7684\u5DE5\u4F5C\u662F\u5BA1\u67E5\u78B3\u57FA\u8282\u70B9\u63D0\u4EA4\u7684\u6240\u6709\u4EFB\u52A1\u4EA4\u4ED8\u7269\uFF0C\u751F\u6210\u6574\u4F53\u5BA1\u67E5\u62A5\u544A\u3002
1134
+ function buildReviewSystemPrompt(ratingSystem) {
1135
+ let base = `\u4F60\u662F HumanClaw \u4EA4\u4ED8\u5BA1\u67E5\u5458\u3002\u4F60\u7684\u5DE5\u4F5C\u662F\u5BA1\u67E5\u78B3\u57FA\u8282\u70B9\u63D0\u4EA4\u7684\u6240\u6709\u4EFB\u52A1\u4EA4\u4ED8\u7269\uFF0C\u751F\u6210\u6574\u4F53\u5BA1\u67E5\u62A5\u544A\u3002
863
1136
 
864
1137
  \u5BA1\u67E5\u8981\u70B9\uFF1A
865
1138
  1. \u68C0\u67E5\u6BCF\u4E2A\u4EA4\u4ED8\u7269\u662F\u5426\u6EE1\u8DB3\u539F\u59CB\u4EFB\u52A1\u8981\u6C42
@@ -870,6 +1143,30 @@ function buildReviewSystemPrompt() {
870
1143
  \u8F93\u51FA\u683C\u5F0F\uFF1A
871
1144
  - \u4F7F\u7528 Markdown \u683C\u5F0F
872
1145
  - \u5148\u7ED9\u603B\u7ED3\u8BC4\u5206\uFF0C\u518D\u9010\u4E2A\u5206\u6790\u6BCF\u4E2A\u5B50\u4EFB\u52A1\u7684\u4EA4\u4ED8\u8D28\u91CF`;
1146
+ if (ratingSystem) {
1147
+ const ratings = RATING_SYSTEMS[ratingSystem];
1148
+ const systemLabels = {
1149
+ ali: "\u963F\u91CC\u7EE9\u6548\u4F53\u7CFB",
1150
+ letter: "SABCD \u7B49\u7EA7",
1151
+ em: "EM/MM \u7EE9\u6548\u4F53\u7CFB"
1152
+ };
1153
+ base += `
1154
+
1155
+ \u540C\u65F6\uFF0C\u8BF7\u4E3A\u6BCF\u4E2A\u53C2\u4E0E\u7684\u78B3\u57FA\u8282\u70B9\u751F\u6210\u4E2A\u4EBA\u7EE9\u6548\u8BC4\u4EF7\u3002
1156
+ \u4F7F\u7528\u300C${systemLabels[ratingSystem]}\u300D\uFF0C\u53EF\u9009\u8BC4\u5206\u4E3A: ${ratings.join(" / ")}
1157
+ \u5728\u5BA1\u67E5\u62A5\u544A\u672B\u5C3E\uFF0C\u989D\u5916\u8F93\u51FA\u4E00\u4E2A JSON \u5757\uFF08\u7528 \`\`\`json \u5305\u88F9\uFF09\uFF0C\u683C\u5F0F\u5982\u4E0B\uFF1A
1158
+ \`\`\`json
1159
+ [
1160
+ {
1161
+ "agent_id": "\u6267\u884C\u8005ID",
1162
+ "trace_id": "\u4EFB\u52A1\u8FFD\u8E2A\u7801",
1163
+ "rating": "\u8BC4\u5206",
1164
+ "comment": "\u4E00\u53E5\u8BDD\u8BC4\u8BED"
1165
+ }
1166
+ ]
1167
+ \`\`\``;
1168
+ }
1169
+ return base;
873
1170
  }
874
1171
  function buildReviewUserPrompt(originalPrompt, tasks) {
875
1172
  let prompt = `\u539F\u59CB\u9700\u6C42: ${originalPrompt}
@@ -890,10 +1187,16 @@ function buildReviewUserPrompt(originalPrompt, tasks) {
890
1187
  }
891
1188
  prompt += `--- \u5B50\u4EFB\u52A1 ${i + 1} ---
892
1189
  `;
893
- prompt += `\u6267\u884C\u8005: ${t.assignee_id}
1190
+ prompt += `\u6267\u884C\u8005: ${t.assignee_name} (${t.assignee_id})
1191
+ `;
1192
+ prompt += `\u8FFD\u8E2A\u7801: ${t.trace_id}
894
1193
  `;
895
1194
  prompt += `\u4EFB\u52A1: ${t.todo_description}
896
1195
  `;
1196
+ if (t.weight !== void 0 && t.weight !== 1) {
1197
+ prompt += `\u4EFB\u52A1\u6743\u91CD: ${t.weight}
1198
+ `;
1199
+ }
897
1200
  prompt += `\u4EA4\u4ED8\u7269:
898
1201
  ${resultText}
899
1202
 
@@ -902,7 +1205,22 @@ ${resultText}
902
1205
  prompt += "\u8BF7\u5BA1\u67E5\u4EE5\u4E0A\u6240\u6709\u4EA4\u4ED8\u7269\uFF0C\u751F\u6210\u5BA1\u67E5\u62A5\u544A\u3002";
903
1206
  return prompt;
904
1207
  }
905
- async function reviewJob(jobId, provider, db2) {
1208
+ function extractEvaluationJson(raw) {
1209
+ const blocks = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/g);
1210
+ if (!blocks) return void 0;
1211
+ const lastBlock = blocks[blocks.length - 1];
1212
+ const jsonMatch = lastBlock.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
1213
+ if (!jsonMatch) return void 0;
1214
+ try {
1215
+ const parsed = JSON.parse(jsonMatch[1].trim());
1216
+ if (Array.isArray(parsed)) {
1217
+ return parsed;
1218
+ }
1219
+ } catch {
1220
+ }
1221
+ return void 0;
1222
+ }
1223
+ async function reviewJob(jobId, provider, db2, ratingSystem, taskWeights) {
906
1224
  const conn = db2 ?? getDb();
907
1225
  const job = getJobWithTasks(jobId, conn);
908
1226
  if (!job) {
@@ -915,38 +1233,58 @@ async function reviewJob(jobId, provider, db2) {
915
1233
  );
916
1234
  }
917
1235
  const llm = provider ?? createLlmProvider();
1236
+ const tasksWithNames = job.tasks.map((t) => {
1237
+ const agent = getAgent(t.assignee_id, conn);
1238
+ return {
1239
+ trace_id: t.trace_id,
1240
+ assignee_id: t.assignee_id,
1241
+ assignee_name: agent?.name || t.assignee_id,
1242
+ todo_description: t.todo_description,
1243
+ result_data: t.result_data,
1244
+ weight: taskWeights?.[t.trace_id]
1245
+ };
1246
+ });
918
1247
  const response = await llm.complete({
919
1248
  messages: [
920
- { role: "system", content: buildReviewSystemPrompt() },
1249
+ { role: "system", content: buildReviewSystemPrompt(ratingSystem) },
921
1250
  {
922
1251
  role: "user",
923
- content: buildReviewUserPrompt(
924
- job.original_prompt,
925
- job.tasks.map((t) => ({
926
- assignee_id: t.assignee_id,
927
- todo_description: t.todo_description,
928
- result_data: t.result_data
929
- }))
930
- )
1252
+ content: buildReviewUserPrompt(job.original_prompt, tasksWithNames)
931
1253
  }
932
1254
  ],
933
1255
  temperature: 0.3,
934
1256
  max_tokens: 4096
935
1257
  });
936
- return {
1258
+ const result = {
937
1259
  job_id: jobId,
938
1260
  original_prompt: job.original_prompt,
939
1261
  review: response.content,
940
1262
  reviewed_at: (/* @__PURE__ */ new Date()).toISOString()
941
1263
  };
1264
+ if (ratingSystem) {
1265
+ const evalData = extractEvaluationJson(response.content);
1266
+ if (evalData) {
1267
+ result.evaluations = evalData.map((e) => {
1268
+ const agent = getAgent(e.agent_id, conn);
1269
+ return {
1270
+ ...e,
1271
+ agent_name: agent?.name || e.agent_id
1272
+ };
1273
+ });
1274
+ }
1275
+ }
1276
+ return result;
942
1277
  }
943
1278
 
944
1279
  // src/routes/sync.ts
945
1280
  var router4 = Router4();
946
1281
  router4.post("/:job_id/review", async (req, res) => {
947
1282
  const { job_id } = req.params;
1283
+ const { rating_system, task_weights } = req.body;
1284
+ const validSystems = ["ali", "letter", "em"];
1285
+ const ratingSystem = rating_system && validSystems.includes(rating_system) ? rating_system : void 0;
948
1286
  try {
949
- const result = await reviewJob(job_id);
1287
+ const result = await reviewJob(job_id, void 0, void 0, ratingSystem, task_weights);
950
1288
  res.json(result);
951
1289
  } catch (error) {
952
1290
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -960,7 +1298,8 @@ var sync_default = router4;
960
1298
  import { Router as Router5 } from "express";
961
1299
  var router5 = Router5();
962
1300
  router5.get("/", (_req, res) => {
963
- const provider = getConfig("llm_provider") || process.env.HUMANCLAW_LLM_PROVIDER || "claude";
1301
+ const rawProvider = getConfig("llm_provider") || process.env.HUMANCLAW_LLM_PROVIDER || "anthropic";
1302
+ const provider = rawProvider === "claude" ? "anthropic" : rawProvider;
964
1303
  const apiKey = getConfig("llm_api_key") || process.env.HUMANCLAW_LLM_API_KEY || "";
965
1304
  const model = getConfig("llm_model") || process.env.HUMANCLAW_LLM_MODEL || "";
966
1305
  const baseUrl = getConfig("llm_base_url") || process.env.HUMANCLAW_LLM_BASE_URL || "";
@@ -977,11 +1316,13 @@ router5.get("/", (_req, res) => {
977
1316
  router5.put("/", (req, res) => {
978
1317
  const { provider, api_key, model, base_url } = req.body;
979
1318
  if (provider !== void 0) {
980
- if (provider !== "claude" && provider !== "openai") {
981
- res.status(400).json({ error: "\u4E0D\u652F\u6301\u7684\u63D0\u4F9B\u5546\uFF0C\u652F\u6301: claude, openai" });
1319
+ const normalized = provider === "claude" ? "anthropic" : provider;
1320
+ const validFormats = ["openai", "anthropic", "responses"];
1321
+ if (!validFormats.includes(normalized)) {
1322
+ res.status(400).json({ error: `\u4E0D\u652F\u6301\u7684 API \u683C\u5F0F\u3002\u652F\u6301: ${validFormats.join(", ")}` });
982
1323
  return;
983
1324
  }
984
- setConfig("llm_provider", provider);
1325
+ setConfig("llm_provider", normalized);
985
1326
  }
986
1327
  if (api_key !== void 0) {
987
1328
  if (api_key === "") {
@@ -1008,6 +1349,295 @@ router5.put("/", (req, res) => {
1008
1349
  });
1009
1350
  var config_default = router5;
1010
1351
 
1352
+ // src/routes/teams.ts
1353
+ import { Router as Router6 } from "express";
1354
+ var router6 = Router6();
1355
+ router6.get("/", (_req, res) => {
1356
+ const teams = listTeams();
1357
+ const teamsWithMembers = teams.map((t) => {
1358
+ const full = getTeamWithMembers(t.team_id);
1359
+ return full || { ...t, members: [] };
1360
+ });
1361
+ res.json({ teams: teamsWithMembers });
1362
+ });
1363
+ router6.get("/:id", (req, res) => {
1364
+ const team = getTeamWithMembers(req.params.id);
1365
+ if (!team) {
1366
+ res.status(404).json({ error: `\u56E2\u961F\u4E0D\u5B58\u5728: ${req.params.id}` });
1367
+ return;
1368
+ }
1369
+ res.json(team);
1370
+ });
1371
+ router6.post("/", (req, res) => {
1372
+ const { name, description } = req.body;
1373
+ if (!name || typeof name !== "string") {
1374
+ res.status(400).json({ error: "name is required" });
1375
+ return;
1376
+ }
1377
+ const team = createTeam({
1378
+ team_id: generateId("team"),
1379
+ name,
1380
+ description: description || ""
1381
+ });
1382
+ res.status(201).json(team);
1383
+ });
1384
+ router6.delete("/:id", (req, res) => {
1385
+ const deleted = deleteTeam(req.params.id);
1386
+ if (!deleted) {
1387
+ res.status(404).json({ error: `\u56E2\u961F\u4E0D\u5B58\u5728: ${req.params.id}` });
1388
+ return;
1389
+ }
1390
+ res.json({ deleted: req.params.id });
1391
+ });
1392
+ router6.post("/:id/members", (req, res) => {
1393
+ const { agent_id, relationship } = req.body;
1394
+ if (!agent_id || typeof agent_id !== "string") {
1395
+ res.status(400).json({ error: "agent_id is required" });
1396
+ return;
1397
+ }
1398
+ const agent = getAgent(agent_id);
1399
+ if (!agent) {
1400
+ res.status(404).json({ error: `Agent \u4E0D\u5B58\u5728: ${agent_id}` });
1401
+ return;
1402
+ }
1403
+ try {
1404
+ addTeamMember(req.params.id, agent_id, relationship || "");
1405
+ res.status(201).json({ team_id: req.params.id, agent_id, relationship: relationship || "" });
1406
+ } catch (error) {
1407
+ const message = error instanceof Error ? error.message : "Unknown error";
1408
+ res.status(400).json({ error: message });
1409
+ }
1410
+ });
1411
+ router6.delete("/:id/members/:agent_id", (req, res) => {
1412
+ const removed = removeTeamMember(req.params.id, req.params.agent_id);
1413
+ if (!removed) {
1414
+ res.status(404).json({ error: "\u6210\u5458\u4E0D\u5728\u8BE5\u56E2\u961F\u4E2D" });
1415
+ return;
1416
+ }
1417
+ res.json({ removed: req.params.agent_id, team_id: req.params.id });
1418
+ });
1419
+ router6.put("/:id/members/:agent_id", (req, res) => {
1420
+ const { relationship } = req.body;
1421
+ if (relationship === void 0 || typeof relationship !== "string") {
1422
+ res.status(400).json({ error: "relationship is required" });
1423
+ return;
1424
+ }
1425
+ const updated = updateTeamMemberRelationship(req.params.id, req.params.agent_id, relationship);
1426
+ if (!updated) {
1427
+ res.status(404).json({ error: "\u6210\u5458\u4E0D\u5728\u8BE5\u56E2\u961F\u4E2D" });
1428
+ return;
1429
+ }
1430
+ res.json({ team_id: req.params.id, agent_id: req.params.agent_id, relationship });
1431
+ });
1432
+ var teams_default = router6;
1433
+
1434
+ // src/routes/evaluations.ts
1435
+ import { Router as Router7 } from "express";
1436
+
1437
+ // src/services/evaluator.ts
1438
+ function generateEvalId() {
1439
+ return "eval_" + Math.random().toString(36).substring(2, 10);
1440
+ }
1441
+ function buildEvalPrompt(originalPrompt, ratingSystem, tasks) {
1442
+ const ratings = RATING_SYSTEMS[ratingSystem];
1443
+ const systemLabels = {
1444
+ ali: "\u963F\u91CC\u7EE9\u6548\u4F53\u7CFB\uFF083.25=\u4E0D\u5408\u683C, 3.5=\u57FA\u672C\u8FBE\u6807, 3.5+=\u8FBE\u6807, 3.75=\u4F18\u79C0, 4.0=\u5353\u8D8A\uFF09",
1445
+ letter: "SABCD \u7B49\u7EA7\uFF08S=\u5353\u8D8A, A=\u4F18\u79C0, B=\u8FBE\u6807, C=\u5F85\u6539\u8FDB, D=\u4E0D\u5408\u683C\uFF09",
1446
+ em: "EM/MM \u4F53\u7CFB\uFF08EM+=\u8FDC\u8D85\u9884\u671F, EM=\u8D85\u51FA\u9884\u671F, MM+=\u8FBE\u6807, MM=\u57FA\u672C\u8FBE\u6807, MM-=\u4E0D\u8FBE\u6807\uFF09"
1447
+ };
1448
+ let prompt = `\u539F\u59CB\u9700\u6C42: ${originalPrompt}
1449
+
1450
+ `;
1451
+ prompt += `\u8BC4\u5206\u4F53\u7CFB: ${systemLabels[ratingSystem]}
1452
+ `;
1453
+ prompt += `\u53EF\u9009\u8BC4\u5206: ${ratings.join(" / ")}
1454
+
1455
+ `;
1456
+ for (const t of tasks) {
1457
+ let resultText = "";
1458
+ if (t.result_data) {
1459
+ if (typeof t.result_data === "string") {
1460
+ resultText = t.result_data;
1461
+ } else if (typeof t.result_data === "object") {
1462
+ const rd = t.result_data;
1463
+ resultText = rd.text || JSON.stringify(rd, null, 2);
1464
+ } else {
1465
+ resultText = String(t.result_data);
1466
+ }
1467
+ }
1468
+ prompt += `--- \u4EFB\u52A1 ${t.trace_id} (\u6743\u91CD: ${t.weight}) ---
1469
+ `;
1470
+ prompt += `\u6267\u884C\u8005: ${t.assignee_name} (${t.assignee_id})
1471
+ `;
1472
+ prompt += `\u4EFB\u52A1: ${t.todo_description}
1473
+ `;
1474
+ prompt += `\u4EA4\u4ED8\u7269:
1475
+ ${resultText}
1476
+
1477
+ `;
1478
+ }
1479
+ prompt += `\u8BF7\u4E3A\u6BCF\u4E2A\u6267\u884C\u8005\u7684\u6BCF\u4E2A\u4EFB\u52A1\u5355\u72EC\u8BC4\u5206\u3002\u4E25\u683C\u8F93\u51FA JSON \u6570\u7EC4\uFF08\u4E0D\u8981\u8F93\u51FA\u5176\u4ED6\u5185\u5BB9\uFF09\uFF1A
1480
+ \`\`\`json
1481
+ [
1482
+ {
1483
+ "agent_id": "\u6267\u884C\u8005ID",
1484
+ "trace_id": "\u4EFB\u52A1\u8FFD\u8E2A\u7801",
1485
+ "rating": "\u4ECE ${ratings.join("/")} \u4E2D\u9009\u4E00\u4E2A",
1486
+ "comment": "\u4E00\u53E5\u8BDD\u8BC4\u8BED\uFF0C\u8BF4\u660E\u7ED9\u6B64\u8BC4\u5206\u7684\u539F\u56E0"
1487
+ }
1488
+ ]
1489
+ \`\`\``;
1490
+ return prompt;
1491
+ }
1492
+ function extractJson2(raw) {
1493
+ const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
1494
+ if (codeBlockMatch) {
1495
+ return codeBlockMatch[1].trim();
1496
+ }
1497
+ const arrayMatch = raw.match(/\[[\s\S]*\]/);
1498
+ if (arrayMatch) {
1499
+ return arrayMatch[0];
1500
+ }
1501
+ return raw.trim();
1502
+ }
1503
+ async function generateEvaluations(request, provider, db2) {
1504
+ const conn = db2 ?? getDb();
1505
+ const job = getJobWithTasks(request.job_id, conn);
1506
+ if (!job) {
1507
+ throw new Error(`Job not found: ${request.job_id}`);
1508
+ }
1509
+ if (!isJobComplete(request.job_id, conn)) {
1510
+ const resolved = job.tasks.filter((t) => t.status === "RESOLVED").length;
1511
+ throw new Error(
1512
+ `Job \u5C1A\u672A\u5168\u90E8\u5B8C\u6210: ${resolved}/${job.tasks.length} \u4E2A\u4EFB\u52A1\u5DF2\u4EA4\u4ED8`
1513
+ );
1514
+ }
1515
+ deleteEvaluationsByJob(request.job_id, conn);
1516
+ const llm = provider ?? createLlmProvider();
1517
+ const tasksWithInfo = job.tasks.map((t) => {
1518
+ const agent = getAgent(t.assignee_id, conn);
1519
+ return {
1520
+ trace_id: t.trace_id,
1521
+ assignee_id: t.assignee_id,
1522
+ assignee_name: agent?.name || t.assignee_id,
1523
+ todo_description: t.todo_description,
1524
+ result_data: t.result_data,
1525
+ weight: request.task_weights?.[t.trace_id] ?? 1
1526
+ };
1527
+ });
1528
+ const response = await llm.complete({
1529
+ messages: [
1530
+ {
1531
+ role: "system",
1532
+ content: "\u4F60\u662F\u4E00\u4E2A\u5BA2\u89C2\u516C\u6B63\u7684\u7EE9\u6548\u8BC4\u4F30\u4E13\u5BB6\u3002\u6839\u636E\u4EFB\u52A1\u5B8C\u6210\u8D28\u91CF\u548C\u4EA4\u4ED8\u7269\u5185\u5BB9\u8FDB\u884C\u8BC4\u5206\u3002"
1533
+ },
1534
+ {
1535
+ role: "user",
1536
+ content: buildEvalPrompt(job.original_prompt, request.rating_system, tasksWithInfo)
1537
+ }
1538
+ ],
1539
+ temperature: 0.3,
1540
+ max_tokens: 4096
1541
+ });
1542
+ const jsonStr = extractJson2(response.content);
1543
+ let parsed;
1544
+ try {
1545
+ parsed = JSON.parse(jsonStr);
1546
+ } catch {
1547
+ throw new Error("AI \u8FD4\u56DE\u7684\u7EE9\u6548\u8BC4\u4EF7\u65E0\u6CD5\u89E3\u6790\u4E3A JSON\uFF0C\u8BF7\u91CD\u8BD5");
1548
+ }
1549
+ if (!Array.isArray(parsed)) {
1550
+ throw new Error("AI \u8FD4\u56DE\u7684\u4E0D\u662F\u6709\u6548\u7684\u8BC4\u4EF7\u6570\u7EC4");
1551
+ }
1552
+ const evaluations = [];
1553
+ for (const item of parsed) {
1554
+ const agentId = String(item.agent_id || "");
1555
+ const traceId = String(item.trace_id || "");
1556
+ let rating = String(item.rating || "");
1557
+ const comment = String(item.comment || "");
1558
+ if (!validateRating(request.rating_system, rating)) {
1559
+ const ratings = RATING_SYSTEMS[request.rating_system];
1560
+ rating = ratings[Math.floor(ratings.length / 2)];
1561
+ }
1562
+ const weight = request.task_weights?.[traceId] ?? 1;
1563
+ const evaluation = createEvaluation({
1564
+ eval_id: generateEvalId(),
1565
+ job_id: request.job_id,
1566
+ agent_id: agentId,
1567
+ trace_id: traceId,
1568
+ rating_system: request.rating_system,
1569
+ rating,
1570
+ weight,
1571
+ comment
1572
+ }, conn);
1573
+ evaluations.push(evaluation);
1574
+ }
1575
+ return {
1576
+ job_id: request.job_id,
1577
+ evaluations,
1578
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
1579
+ };
1580
+ }
1581
+ function getPerformanceDashboard(agentId, db2) {
1582
+ const conn = db2 ?? getDb();
1583
+ if (agentId) {
1584
+ return { evaluations: listEvaluationsByAgent(agentId, conn) };
1585
+ }
1586
+ const rows = conn.prepare(
1587
+ `SELECT e.*, j.original_prompt, t.todo_description, a.name AS agent_name
1588
+ FROM evaluations e
1589
+ LEFT JOIN jobs j ON e.job_id = j.job_id
1590
+ LEFT JOIN tasks t ON e.trace_id = t.trace_id
1591
+ LEFT JOIN agents a ON e.agent_id = a.agent_id
1592
+ ORDER BY e.created_at DESC
1593
+ LIMIT 50`
1594
+ ).all();
1595
+ return { evaluations: rows };
1596
+ }
1597
+
1598
+ // src/routes/evaluations.ts
1599
+ var router7 = Router7();
1600
+ router7.post("/generate", async (req, res) => {
1601
+ const { job_id, rating_system, task_weights } = req.body;
1602
+ if (!job_id || typeof job_id !== "string") {
1603
+ res.status(400).json({ error: "job_id is required" });
1604
+ return;
1605
+ }
1606
+ const validSystems = ["ali", "letter", "em"];
1607
+ if (!rating_system || !validSystems.includes(rating_system)) {
1608
+ res.status(400).json({
1609
+ error: `rating_system is required. Must be one of: ${validSystems.join(", ")}`
1610
+ });
1611
+ return;
1612
+ }
1613
+ try {
1614
+ const result = await generateEvaluations({
1615
+ job_id,
1616
+ rating_system,
1617
+ task_weights
1618
+ });
1619
+ res.json(result);
1620
+ } catch (error) {
1621
+ const message = error instanceof Error ? error.message : "Unknown error";
1622
+ const status = message.includes("API Key") || message.includes("API key") ? 503 : 400;
1623
+ res.status(status).json({ error: message });
1624
+ }
1625
+ });
1626
+ router7.get("/job/:job_id", (req, res) => {
1627
+ const evaluations = listEvaluationsByJob(req.params.job_id);
1628
+ res.json({ evaluations });
1629
+ });
1630
+ router7.get("/agent/:agent_id", (req, res) => {
1631
+ const evaluations = listEvaluationsByAgent(req.params.agent_id);
1632
+ res.json({ evaluations });
1633
+ });
1634
+ router7.get("/dashboard", (req, res) => {
1635
+ const agentId = req.query.agent_id;
1636
+ const dashboard = getPerformanceDashboard(agentId);
1637
+ res.json(dashboard);
1638
+ });
1639
+ var evaluations_default = router7;
1640
+
1011
1641
  // src/dashboard.ts
1012
1642
  function getDashboardHtml() {
1013
1643
  return `<!DOCTYPE html>
@@ -1164,6 +1794,23 @@ main{padding:24px 32px;max-width:1200px}
1164
1794
  .task-row .remove-task{position:absolute;top:8px;right:10px;background:none;border:none;color:var(--red);cursor:pointer;font-size:16px;line-height:1}
1165
1795
  .task-row .fg{margin-bottom:10px}
1166
1796
  .task-row .fg:last-child{margin-bottom:0}
1797
+ /* Markdown Editor */
1798
+ .md-editor{position:relative}
1799
+ .md-editor textarea{min-height:120px;resize:vertical;font-family:var(--font-mono);font-size:12px;width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);outline:none;transition:border-color .15s}
1800
+ .md-editor textarea:focus{border-color:var(--accent)}
1801
+ .md-expand-btn{position:absolute;top:6px;right:6px;background:var(--surface-hover);border:1px solid var(--border);color:var(--text-dim);border-radius:4px;width:24px;height:24px;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;transition:all .15s;line-height:1}
1802
+ .md-expand-btn:hover{color:var(--accent);border-color:var(--accent)}
1803
+ .md-editor.fullscreen{position:fixed;inset:20px;z-index:200;background:var(--surface);border-radius:var(--radius);padding:16px;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.7)}
1804
+ .md-editor.fullscreen textarea{flex:1;min-height:unset;height:100%;resize:none;font-size:14px}
1805
+ .md-editor.fullscreen .md-expand-btn{top:22px;right:22px}
1806
+ .md-fullscreen-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:199}
1807
+ /* Team badge */
1808
+ .team-badge{display:inline-block;background:rgba(168,85,247,.12);border:1px solid rgba(168,85,247,.25);border-radius:4px;padding:1px 8px;font-size:10px;color:var(--purple);margin-right:4px;margin-bottom:2px}
1809
+ /* Evaluation */
1810
+ .eval-badge{display:inline-block;padding:3px 10px;border-radius:12px;font-weight:700;font-size:13px;font-family:var(--font-mono)}
1811
+ .eval-badge.top{background:rgba(34,197,94,.15);color:var(--green)}
1812
+ .eval-badge.mid{background:rgba(234,179,8,.15);color:var(--yellow)}
1813
+ .eval-badge.low{background:rgba(239,68,68,.15);color:var(--red)}
1167
1814
  </style>
1168
1815
  </head>
1169
1816
  <body>
@@ -1193,8 +1840,25 @@ function toast(msg,ok){
1193
1840
  setTimeout(()=>t.remove(),3500);
1194
1841
  }
1195
1842
  let cachedAgents=[];
1843
+ let cachedTeams=[];
1196
1844
  let currentPlan=null;
1197
1845
  let selectedAgentIds=new Set();
1846
+ let selectedTeamId='';
1847
+
1848
+ window.toggleFullscreen=function(btn){
1849
+ const editor=btn.closest('.md-editor');
1850
+ if(editor.classList.contains('fullscreen')){
1851
+ editor.classList.remove('fullscreen');
1852
+ const bd=document.querySelector('.md-fullscreen-backdrop');
1853
+ if(bd)bd.remove();
1854
+ }else{
1855
+ const bd=document.createElement('div');bd.className='md-fullscreen-backdrop';
1856
+ bd.onclick=()=>{editor.classList.remove('fullscreen');bd.remove()};
1857
+ document.body.appendChild(bd);
1858
+ editor.classList.add('fullscreen');
1859
+ const ta=editor.querySelector('textarea');if(ta)ta.focus();
1860
+ }
1861
+ };
1198
1862
 
1199
1863
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1200
1864
  // DEMO SCENARIOS
@@ -1204,6 +1868,10 @@ const DEMOS={
1204
1868
  emoji:'&#128009;',title:'\u4E09\u56FD\u8700\u6C49',role:'\u4F60\u662F\u5218\u5907',
1205
1869
  desc:'\u6843\u56ED\u7ED3\u4E49\uFF0C\u4E09\u987E\u8305\u5E90\u3002\u4F5C\u4E3A\u8700\u6C49\u4E4B\u4E3B\uFF0C\u7EDF\u9886\u4E94\u864E\u4E0A\u5C06\u548C\u5367\u9F99\u51E4\u96CF\uFF0C\u9010\u9E7F\u4E2D\u539F\u3002',
1206
1870
  prompt:'\u5317\u4F10\u4E2D\u539F\uFF0C\u5175\u5206\u4E09\u8DEF\uFF0C\u9700\u8981\u653B\u57CE\u3001\u65AD\u7CAE\u548C\u5916\u4EA4\u4E09\u7BA1\u9F50\u4E0B',
1871
+ teams:[
1872
+ {name:'\u4E94\u864E\u4E0A\u5C06',description:'\u8700\u6C49\u4E94\u5927\u731B\u5C06',members:['\u5173\u7FBD','\u5F20\u98DE','\u8D75\u4E91','\u9EC4\u5FE0','\u9A6C\u8D85'],relationships:{'\u5173\u7FBD':'\u4E49\u5F1F\u517C\u519B\u56E2\u7EDF\u5E05','\u5F20\u98DE':'\u4E49\u5F1F\u517C\u5148\u950B\u5927\u5C06','\u8D75\u4E91':'\u5FC3\u8179\u7231\u5C06','\u9EC4\u5FE0':'\u8001\u5F53\u76CA\u58EE\u7684\u731B\u5C06','\u9A6C\u8D85':'\u5F52\u964D\u7684\u897F\u51C9\u864E\u5C06'}},
1873
+ {name:'\u8C0B\u58EB\u56E2',description:'\u8FD0\u7B79\u5E37\u5E44\u7684\u667A\u56CA\u56E2',members:['\u8BF8\u845B\u4EAE','\u5E9E\u7EDF'],relationships:{'\u8BF8\u845B\u4EAE':'\u9996\u5E2D\u519B\u5E08\uFF0C\u5982\u9C7C\u5F97\u6C34','\u5E9E\u7EDF':'\u526F\u519B\u5E08\uFF0C\u5947\u8C0B\u767E\u51FA'}}
1874
+ ],
1207
1875
  agents:[
1208
1876
  {name:'\u5173\u7FBD',capabilities:['\u6B66\u827A','\u7EDF\u5175','\u9547\u5B88\u8981\u5730','\u6C34\u519B\u6307\u6325'],relationship:'\u4E49\u5F1F\uFF0C\u6843\u56ED\u7ED3\u4E49\u4E8C\u5F1F\uFF0C\u6700\u4FE1\u4EFB\u7684\u5144\u5F1F\u548C\u5927\u5C06'},
1209
1877
  {name:'\u5F20\u98DE',capabilities:['\u6B66\u827A','\u5148\u950B\u7A81\u51FB','\u9A91\u5175\u6307\u6325','\u5A01\u6151\u654C\u519B'],relationship:'\u4E49\u5F1F\uFF0C\u6843\u56ED\u7ED3\u4E49\u4E09\u5F1F\uFF0C\u6027\u5982\u70C8\u706B\u4F46\u5FE0\u5FC3\u803F\u803F'},
@@ -1218,6 +1886,12 @@ const DEMOS={
1218
1886
  emoji:'&#128187;',title:'\u4E92\u8054\u7F51\u5927\u5382',role:'\u4F60\u662F\u6280\u672F\u603B\u76D1',
1219
1887
  desc:'\u5E26\u9886\u4E00\u652F\u5168\u6808\u56E2\u961F\uFF0C\u4ECE\u524D\u7AEF\u5230\u8FD0\u7EF4\u4E00\u5E94\u4FF1\u5168\u3002\u5E94\u5BF9\u9AD8\u5E76\u53D1\u3001\u641E AI\u3001\u4E0A\u7EBF\u65B0\u7CFB\u7EDF\u3002',
1220
1888
  prompt:'\u4E0A\u7EBF\u4E00\u4E2A AI \u667A\u80FD\u5BA2\u670D\u7CFB\u7EDF\uFF0C\u5305\u62EC\u524D\u7AEF\u754C\u9762\u3001\u540E\u7AEF API\u3001\u63A8\u8350\u7B97\u6CD5\u3001\u538B\u529B\u6D4B\u8BD5\u548C\u7070\u5EA6\u53D1\u5E03\u65B9\u6848',
1889
+ teams:[
1890
+ {name:'\u524D\u7AEF\u7EC4',description:'\u8D1F\u8D23\u6240\u6709\u524D\u7AEF\u9875\u9762\u5F00\u53D1',members:['\u524D\u7AEF\u8001\u674E','\u8BBE\u8BA1\u5E08\u5C0F\u6797'],relationships:{'\u524D\u7AEF\u8001\u674E':'\u524D\u7AEFTL\uFF0C\u6838\u5FC3\u9AA8\u5E72','\u8BBE\u8BA1\u5E08\u5C0F\u6797':'\u8D44\u6DF1\u8BBE\u8BA1\u5E08'}},
1891
+ {name:'\u540E\u7AEF\u7EC4',description:'\u540E\u7AEF\u67B6\u6784\u4E0E\u670D\u52A1\u5F00\u53D1',members:['\u540E\u7AEF\u5927\u738B','\u7B97\u6CD5\u5C0F\u9648'],relationships:{'\u540E\u7AEF\u5927\u738B':'\u67B6\u6784\u5E08\uFF0C\u6280\u672F\u51B3\u7B56\u8005','\u7B97\u6CD5\u5C0F\u9648':'\u7B97\u6CD5\u5DE5\u7A0B\u5E08\uFF0C\u9700\u8981\u6307\u5BFC'}},
1892
+ {name:'\u4EA7\u54C1\u7EC4',description:'\u4EA7\u54C1\u9700\u6C42\u4E0E\u9879\u76EE\u7BA1\u7406',members:['\u4EA7\u54C1\u7ECF\u7406 Amy'],relationships:{'\u4EA7\u54C1\u7ECF\u7406 Amy':'\u4EA7\u54C1\u8D1F\u8D23\u4EBA'}},
1893
+ {name:'\u8D28\u91CF\u7EC4',description:'\u6D4B\u8BD5\u4E0E\u8FD0\u7EF4\u4FDD\u969C',members:['\u6D4B\u8BD5\u8D1F\u8D23\u4EBA\u8001\u8D75','\u8FD0\u7EF4 DevOps \u963F\u6770'],relationships:{'\u6D4B\u8BD5\u8D1F\u8D23\u4EBA\u8001\u8D75':'\u8D28\u91CF\u628A\u5173\u4EBA','\u8FD0\u7EF4 DevOps \u963F\u6770':'SRE\u8D1F\u8D23\u4EBA'}}
1894
+ ],
1221
1895
  agents:[
1222
1896
  {name:'\u524D\u7AEF\u8001\u674E',capabilities:['React','TypeScript','Next.js','\u79FB\u52A8\u7AEF\u9002\u914D','\u6027\u80FD\u4F18\u5316'],relationship:'P7 \u524D\u7AEF TL\uFF0C\u8DDF\u4E86\u4F60\u4E09\u5E74\uFF0C\u6280\u672F\u8FC7\u786C\u4F46\u6700\u8FD1\u6709\u70B9\u5026\u6020'},
1223
1897
  {name:'\u540E\u7AEF\u5927\u738B',capabilities:['Java','Go','\u5FAE\u670D\u52A1','\u6570\u636E\u5E93\u8BBE\u8BA1','\u9AD8\u5E76\u53D1\u67B6\u6784'],relationship:'P8 \u540E\u7AEF\u67B6\u6784\u5E08\uFF0C\u6280\u672F\u5927\u62FF\uFF0C\u8BF4\u8BDD\u76F4\u6765\u76F4\u53BB'},
@@ -1232,6 +1906,11 @@ const DEMOS={
1232
1906
  emoji:'&#127482;&#127480;',title:'\u7F8E\u56FD\u653F\u5E9C',role:'\u4F60\u662F\u7279\u6717\u666E (POTUS)',
1233
1907
  desc:'Make the executive branch great again! \u7BA1\u7406\u4F60\u7684\u6838\u5FC3\u5185\u9601\u6210\u5458\uFF0C\u63A8\u884C\u653F\u7B56\u8BAE\u7A0B\u3002',
1234
1908
  prompt:'\u5236\u5B9A\u4E00\u4E2A\u8BA9\u7F8E\u56FD\u5236\u9020\u4E1A\u56DE\u6D41\u7684\u7EFC\u5408\u8BA1\u5212\uFF0C\u9700\u8981\u5173\u7A0E\u653F\u7B56\u3001\u51CF\u7A0E\u65B9\u6848\u3001\u80FD\u6E90\u4FDD\u969C\u3001\u8FB9\u5883\u5B89\u5168\u914D\u5408\u548C\u653F\u5E9C\u6548\u7387\u4F18\u5316',
1909
+ teams:[
1910
+ {name:'\u7ECF\u6D4E\u5B89\u5168\u56E2\u961F',description:'\u7ECF\u6D4E\u653F\u7B56\u4E0E\u8D22\u653F\u7BA1\u7406',members:['Scott Bessent','Elon Musk'],relationships:{'Scott Bessent':'\u8D22\u653F\u90E8\u957F\uFF0C\u9996\u5E2D\u7ECF\u6D4E\u987E\u95EE','Elon Musk':'DOGE \u8D1F\u8D23\u4EBA\uFF0C\u6548\u7387\u6539\u9769\u63A8\u52A8\u8005'}},
1911
+ {name:'\u56FD\u9632\u5916\u4EA4\u56E2\u961F',description:'\u56FD\u9632\u4E0E\u5916\u4EA4\u4E8B\u52A1',members:['Marco Rubio','Pete Hegseth','Tulsi Gabbard'],relationships:{'Marco Rubio':'\u56FD\u52A1\u537F\uFF0C\u5916\u4EA4\u603B\u7BA1','Pete Hegseth':'\u56FD\u9632\u90E8\u957F\uFF0C\u519B\u4E8B\u4E8B\u52A1','Tulsi Gabbard':'\u60C5\u62A5\u603B\u76D1\uFF0C\u5B89\u5168\u8BC4\u4F30'}},
1912
+ {name:'\u56FD\u5185\u4E8B\u52A1\u56E2\u961F',description:'\u56FD\u5185\u5B89\u5168\u4E0E\u516C\u5171\u670D\u52A1',members:['Kristi Noem','Robert F. Kennedy Jr.'],relationships:{'Kristi Noem':'\u56FD\u571F\u5B89\u5168\u90E8\u957F\uFF0C\u8FB9\u5883\u5F3A\u786C\u6D3E','Robert F. Kennedy Jr.':'\u536B\u751F\u90E8\u957F\uFF0C\u533B\u7597\u6539\u9769'}}
1913
+ ],
1235
1914
  agents:[
1236
1915
  {name:'Elon Musk',capabilities:['\u653F\u5E9C\u6548\u7387','\u6210\u672C\u524A\u51CF','\u79D1\u6280\u521B\u65B0','SpaceX','Tesla','\u793E\u4EA4\u5A92\u4F53'],relationship:'DOGE \u8D1F\u8D23\u4EBA\uFF0C\u4E16\u754C\u9996\u5BCC\uFF0CTwitter/X \u8001\u677F\uFF0C\u6700\u5177\u5F71\u54CD\u529B\u7684\u76DF\u53CB'},
1237
1916
  {name:'Marco Rubio',capabilities:['\u5916\u4EA4\u653F\u7B56','\u62C9\u7F8E\u4E8B\u52A1','\u56FD\u9645\u8C08\u5224','\u5236\u88C1\u653F\u7B56','\u56FD\u5BB6\u5B89\u5168'],relationship:'\u56FD\u52A1\u537F\uFF0C\u4F5B\u7F57\u91CC\u8FBE\u53C2\u8BAE\u5458\uFF0C\u66FE\u7ECF\u7684\u7ADE\u9009\u5BF9\u624B\u53D8\u5FE0\u5B9E\u652F\u6301\u8005'},
@@ -1274,12 +1953,31 @@ window.loadDemo=async function(key){
1274
1953
  }
1275
1954
  let ok=0;
1276
1955
  const demoAgentIds=[];
1956
+ const agentNameToId={};
1277
1957
  for(const a of demo.agents){
1278
1958
  try{
1279
1959
  const r=await fetch(API+'/nodes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:a.name,capabilities:a.capabilities,relationship:a.relationship})});
1280
- if(r.ok){ok++;const d=await r.json();demoAgentIds.push(d.agent_id);}
1960
+ if(r.ok){ok++;const d=await r.json();demoAgentIds.push(d.agent_id);agentNameToId[a.name]=d.agent_id;}
1281
1961
  }catch{}
1282
1962
  }
1963
+ // Create demo teams
1964
+ if(demo.teams){
1965
+ for(const tm of demo.teams){
1966
+ try{
1967
+ const tr=await fetch(API+'/teams',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:tm.name,description:tm.description||''})});
1968
+ if(tr.ok){
1969
+ const td=await tr.json();
1970
+ for(const mName of tm.members){
1971
+ const agentId=agentNameToId[mName];
1972
+ if(agentId){
1973
+ const rel=tm.relationships?tm.relationships[mName]||'':'';
1974
+ await fetch(API+'/teams/'+td.team_id+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent_id:agentId,relationship:rel})});
1975
+ }
1976
+ }
1977
+ }
1978
+ }catch{}
1979
+ }
1980
+ }
1283
1981
  toast(demo.title+' \u573A\u666F\u5DF2\u52A0\u8F7D\uFF01'+ok+'/'+demo.agents.length+' \u4E2A\u8282\u70B9\u6CE8\u518C\u6210\u529F',true);
1284
1982
  // Switch to pipeline and open AI planning with suggested prompt
1285
1983
  load('fleet');
@@ -1324,6 +2022,8 @@ async function loadFleet(el){
1324
2022
  const r=await fetch(API+'/nodes/status');
1325
2023
  const d=await r.json();
1326
2024
  cachedAgents=d.agents||[];
2025
+ // Also load teams
2026
+ try{const tr=await fetch(API+'/teams');const td=await tr.json();cachedTeams=td.teams||[]}catch{cachedTeams=[]}
1327
2027
  let h='<div class="section-hd"><h2>\u78B3\u57FA\u7B97\u529B\u6C60</h2><div style="display:flex;gap:6px"><button class="btn btn-ghost btn-sm" onclick="showDemoSelector()">&#127918; Demo</button><button class="btn btn-primary btn-sm" onclick="showAddAgent()">+ \u6DFB\u52A0\u8282\u70B9</button></div></div>';
1328
2028
  h+='<div class="stats">';
1329
2029
  h+='<div class="stat"><div class="stat-val">'+d.total+'</div><div class="stat-lbl">\u603B\u8BA1</div></div>';
@@ -1342,6 +2042,9 @@ async function loadFleet(el){
1342
2042
  h+='<div class="card"><div class="agent-header"><span class="dot '+a.status+'"></span><span class="agent-name">'+esc(a.name)+'</span></div>';
1343
2043
  h+='<div class="agent-id">'+a.agent_id+'</div>';
1344
2044
  if(a.relationship)h+='<div style="font-size:11px;color:var(--purple);margin-bottom:4px">&#128101; '+esc(a.relationship)+'</div>';
2045
+ // Team badges
2046
+ const agentTeams=cachedTeams.filter(tm=>tm.members&&tm.members.some(m=>m.agent_id===a.agent_id));
2047
+ if(agentTeams.length){h+='<div style="margin-bottom:4px">';for(const tm of agentTeams)h+='<span class="team-badge">'+esc(tm.name)+'</span>';h+='</div>'}
1345
2048
  h+='<div class="caps">';for(const c of a.capabilities)h+='<span class="cap">'+esc(c)+'</span>';h+='</div>';
1346
2049
  h+='<div class="agent-meta"><span>\u4EFB\u52A1: '+a.active_task_count+'</span>';
1347
2050
  if(a.avg_delivery_hours!==null)h+='<span>\u5E73\u5747\u4EA4\u4ED8: '+a.avg_delivery_hours+'h</span>';
@@ -1354,6 +2057,23 @@ async function loadFleet(el){
1354
2057
  h+='</div></div>';
1355
2058
  }
1356
2059
  h+='</div>';
2060
+ // Teams section
2061
+ h+='<div style="margin-top:32px"><div class="section-hd"><h2>\u56E2\u961F\u7BA1\u7406</h2><button class="btn btn-primary btn-sm" onclick="showCreateTeam()">+ \u521B\u5EFA\u56E2\u961F</button></div>';
2062
+ if(cachedTeams.length){
2063
+ h+='<div class="grid">';
2064
+ for(const tm of cachedTeams){
2065
+ const memberCount=tm.members?tm.members.length:0;
2066
+ h+='<div class="card" style="cursor:pointer" onclick="showTeamDetail(\\''+tm.team_id+'\\')">';
2067
+ h+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span style="font-size:20px">&#128101;</span><span class="agent-name">'+esc(tm.name)+'</span></div>';
2068
+ if(tm.description)h+='<div style="font-size:12px;color:var(--text-dim);margin-bottom:8px">'+esc(tm.description)+'</div>';
2069
+ h+='<div style="font-size:11px;color:var(--accent)">'+memberCount+' \u4E2A\u6210\u5458</div>';
2070
+ h+='</div>';
2071
+ }
2072
+ h+='</div>';
2073
+ }else{
2074
+ h+='<div style="font-size:12px;color:var(--text-dim);margin-top:8px">\u6682\u65E0\u56E2\u961F\u3002\u521B\u5EFA\u56E2\u961F\u540E\u53EF\u6309\u56E2\u961F\u5206\u914D\u4EFB\u52A1\u3002</div>';
2075
+ }
2076
+ h+='</div>';
1357
2077
  el.innerHTML=h;
1358
2078
  }catch(e){el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u5931\u8D25: '+e.message+'</p></div>'}
1359
2079
  }
@@ -1399,6 +2119,95 @@ window.deleteAgent=async function(id){
1399
2119
  }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1400
2120
  };
1401
2121
 
2122
+ // \u2500\u2500\u2500 Team Management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2123
+ window.showCreateTeam=function(){
2124
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
2125
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
2126
+ let membersHtml='<div id="team-members-list">';
2127
+ for(const a of cachedAgents){
2128
+ membersHtml+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
2129
+ membersHtml+='<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px"><input type="checkbox" class="tm-check" value="'+a.agent_id+'"/><span class="dot '+a.status+'" style="width:8px;height:8px"></span>'+esc(a.name)+'</label>';
2130
+ membersHtml+='<input class="tm-rel" data-agent="'+a.agent_id+'" placeholder="\u56E2\u961F\u4E2D\u7684\u5173\u7CFB" style="flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11px;color:var(--text);outline:none"/>';
2131
+ membersHtml+='</div>';
2132
+ }
2133
+ membersHtml+='</div>';
2134
+ ov.innerHTML='<div class="form-card"><h3>+ \u521B\u5EFA\u56E2\u961F</h3>'
2135
+ +'<div class="fg"><label>\u56E2\u961F\u540D\u79F0</label><input id="tm-name" placeholder="\u4F8B: \u524D\u7AEF\u7EC4"/></div>'
2136
+ +'<div class="fg"><label>\u56E2\u961F\u63CF\u8FF0 <span style="color:var(--text-dim);font-weight:400">(\u53EF\u9009)</span></label><input id="tm-desc" placeholder="\u4F8B: \u8D1F\u8D23\u6240\u6709\u524D\u7AEF\u9875\u9762\u5F00\u53D1"/></div>'
2137
+ +'<div class="fg"><label>\u9009\u62E9\u6210\u5458\u53CA\u56E2\u961F\u5173\u7CFB</label>'+membersHtml+'</div>'
2138
+ +'<div class="btn-group"><button class="btn btn-primary" onclick="submitTeam()">\u521B\u5EFA\u56E2\u961F</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div></div>';
2139
+ document.body.appendChild(ov);
2140
+ document.getElementById('tm-name').focus();
2141
+ };
2142
+ window.submitTeam=async function(){
2143
+ const name=document.getElementById('tm-name').value.trim();
2144
+ const desc=document.getElementById('tm-desc').value.trim();
2145
+ if(!name){toast('\u8BF7\u8F93\u5165\u56E2\u961F\u540D\u79F0',false);return}
2146
+ try{
2147
+ const r=await fetch(API+'/teams',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,description:desc})});
2148
+ const d=await r.json();
2149
+ if(!r.ok){toast(d.error||'\u521B\u5EFA\u5931\u8D25',false);return}
2150
+ const checks=document.querySelectorAll('.tm-check:checked');
2151
+ for(const chk of checks){
2152
+ const agentId=chk.value;
2153
+ const relInput=document.querySelector('.tm-rel[data-agent="'+agentId+'"]');
2154
+ const rel=relInput?relInput.value.trim():'';
2155
+ await fetch(API+'/teams/'+d.team_id+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent_id:agentId,relationship:rel})});
2156
+ }
2157
+ toast('\u56E2\u961F\u300C'+name+'\u300D\u521B\u5EFA\u6210\u529F\uFF01',true);
2158
+ document.getElementById('overlay').remove();
2159
+ load('fleet');
2160
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
2161
+ };
2162
+ window.showTeamDetail=async function(teamId){
2163
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
2164
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
2165
+ ov.innerHTML='<div class="form-card"><h3>\u56E2\u961F\u8BE6\u60C5</h3><div class="spinner-wrap"><div class="spinner"></div></div></div>';
2166
+ document.body.appendChild(ov);
2167
+ try{
2168
+ const r=await fetch(API+'/teams/'+teamId);
2169
+ const tm=await r.json();
2170
+ if(!r.ok){toast(tm.error||'\u52A0\u8F7D\u5931\u8D25',false);ov.remove();return}
2171
+ const fc=ov.querySelector('.form-card');
2172
+ let h='<h3>'+esc(tm.name)+'</h3>';
2173
+ if(tm.description)h+='<div style="font-size:12px;color:var(--text-dim);margin-bottom:16px">'+esc(tm.description)+'</div>';
2174
+ h+='<div style="font-size:11px;color:var(--text-dim);margin-bottom:12px;font-family:var(--font-mono)">'+tm.team_id+'</div>';
2175
+ if(tm.members&&tm.members.length){
2176
+ h+='<div style="font-size:13px;font-weight:600;margin-bottom:8px">\u6210\u5458 ('+tm.members.length+')</div>';
2177
+ for(const m of tm.members){
2178
+ h+='<div style="display:flex;align-items:center;gap:8px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:8px;margin-bottom:6px">';
2179
+ h+='<span class="dot '+(m.agent_status||'IDLE')+'" style="width:8px;height:8px"></span>';
2180
+ h+='<span style="font-size:13px;font-weight:500">'+esc(m.agent_name||m.agent_id)+'</span>';
2181
+ if(m.relationship)h+='<span style="font-size:11px;color:var(--purple);margin-left:auto">'+esc(m.relationship)+'</span>';
2182
+ h+='<button style="margin-left:'+(m.relationship?'8px':'auto')+';background:none;border:none;color:var(--red);cursor:pointer;font-size:14px" onclick="removeTeamMember(\\''+teamId+'\\',\\''+m.agent_id+'\\')">&times;</button>';
2183
+ h+='</div>';
2184
+ }
2185
+ }else{
2186
+ h+='<div style="font-size:12px;color:var(--text-dim)">\u6682\u65E0\u6210\u5458</div>';
2187
+ }
2188
+ h+='<div class="btn-group"><button class="btn btn-danger btn-sm" onclick="deleteTeam(\\''+teamId+'\\')">\u5220\u9664\u56E2\u961F</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u5173\u95ED</button></div>';
2189
+ fc.innerHTML=h;
2190
+ }catch{toast('\u52A0\u8F7D\u5931\u8D25',false);ov.remove()}
2191
+ };
2192
+ window.removeTeamMember=async function(teamId,agentId){
2193
+ try{
2194
+ await fetch(API+'/teams/'+teamId+'/members/'+agentId,{method:'DELETE'});
2195
+ toast('\u6210\u5458\u5DF2\u79FB\u9664',true);
2196
+ document.getElementById('overlay').remove();
2197
+ showTeamDetail(teamId);
2198
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
2199
+ };
2200
+ window.deleteTeam=async function(teamId){
2201
+ if(!confirm('\u786E\u5B9A\u5220\u9664\u6B64\u56E2\u961F\uFF1F'))return;
2202
+ try{
2203
+ const r=await fetch(API+'/teams/'+teamId,{method:'DELETE'});
2204
+ if(!r.ok){toast('\u5220\u9664\u5931\u8D25',false);return}
2205
+ toast('\u56E2\u961F\u5DF2\u5220\u9664',true);
2206
+ document.getElementById('overlay').remove();
2207
+ load('fleet');
2208
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
2209
+ };
2210
+
1402
2211
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1403
2212
  // PIPELINE
1404
2213
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -1430,7 +2239,7 @@ async function loadPipeline(el){
1430
2239
  allTasks.push(...j.tasks);
1431
2240
  h+='<div class="job-card"><div class="job-hd"><span class="job-title">'+esc(j.original_prompt)+'</span><span class="job-id">'+j.job_id+'</span></div>';
1432
2241
  h+='<div class="pbar-wrap"><div class="pbar-label">'+res+'/'+tot+' \u5DF2\u5B8C\u6210 ('+pct+'%)</div><div class="pbar"><div class="pbar-fill" style="width:'+pct+'%"></div></div></div>';
1433
- if(pct===100)h+='<div style="margin-bottom:12px"><button class="btn btn-green btn-sm" onclick="reviewJob(\\''+j.job_id+'\\')">AI \u805A\u5408\u5BA1\u67E5</button></div>';
2242
+ if(pct===100)h+='<div style="margin-bottom:12px;display:flex;gap:6px;flex-wrap:wrap"><button class="btn btn-green btn-sm" onclick="reviewJob(\\''+j.job_id+'\\')">AI \u805A\u5408\u5BA1\u67E5</button><button class="btn btn-primary btn-sm" onclick="showEvalDialog(\\''+j.job_id+'\\')">&#128202; \u751F\u6210\u7EE9\u6548\u8BC4\u4EF7</button></div>';
1434
2243
  h+='<div class="kanban"><div class="lane"><div class="lane-hd y">\u5DF2\u5206\u53D1 ('+dispatched.length+')</div>'+dispatched.map(tcard).join('')+'</div>';
1435
2244
  h+='<div class="lane"><div class="lane-hd r">\u5DF2\u8D85\u65F6 ('+overdue.length+')</div>'+overdue.map(tcard).join('')+'</div>';
1436
2245
  h+='<div class="lane"><div class="lane-hd g">\u5DF2\u4EA4\u4ED8 ('+done.length+')</div>'+done.map(tcard).join('')+'</div></div></div>';
@@ -1488,9 +2297,24 @@ function renderPlanStep1(ov){
1488
2297
  const el=document.getElementById('agent-chip-area');
1489
2298
  if(el)el.innerHTML=renderChips(chipFilter);
1490
2299
  };
2300
+ window._selectTeam=function(teamId){
2301
+ selectedTeamId=teamId;
2302
+ if(teamId){
2303
+ const tm=cachedTeams.find(t=>t.team_id===teamId);
2304
+ if(tm&&tm.members){
2305
+ selectedAgentIds=new Set(tm.members.map(m=>m.agent_id));
2306
+ }
2307
+ }
2308
+ const el=document.getElementById('agent-chip-area');
2309
+ if(el)el.innerHTML=renderChips(chipFilter);
2310
+ // Re-render to show team hint
2311
+ renderPlanStep1(document.getElementById('overlay'));
2312
+ };
1491
2313
 
1492
2314
  ov.innerHTML='<div class="form-card"><h3>AI \u667A\u80FD\u89C4\u5212</h3>'
1493
- +'<div class="fg"><label>\u8F93\u5165\u4F60\u7684\u9700\u6C42</label><textarea id="plan-prompt" rows="3" placeholder="\u4F8B: \u5B8C\u6210\u9996\u9875\u91CD\u6784\uFF0C\u5305\u62EC\u5BFC\u822A\u680F\u3001\u5185\u5BB9\u533A\u548C\u9875\u811A\u7684\u54CD\u5E94\u5F0F\u6539\u7248" style="font-family:var(--font-sans);font-size:13px">'+esc(pendingDemoPrompt)+'</textarea></div>'
2315
+ +'<div class="fg"><label>\u8F93\u5165\u4F60\u7684\u9700\u6C42</label><div class="md-editor"><textarea id="plan-prompt" rows="3" placeholder="\u4F8B: \u5B8C\u6210\u9996\u9875\u91CD\u6784\uFF0C\u5305\u62EC\u5BFC\u822A\u680F\u3001\u5185\u5BB9\u533A\u548C\u9875\u811A\u7684\u54CD\u5E94\u5F0F\u6539\u7248" style="font-family:var(--font-sans);font-size:13px">'+esc(pendingDemoPrompt)+'</textarea><button class="md-expand-btn" onclick="toggleFullscreen(this)" title="\u5C55\u5F00/\u6536\u8D77">&#x26F6;</button></div></div>'
2316
+ +'<div class="fg"><label>\u6309\u56E2\u961F\u9009\u62E9 <span style="color:var(--text-dim);font-weight:400">(\u53EF\u9009)</span></label><select id="plan-team" onchange="window._selectTeam(this.value)"><option value="">-- \u4E0D\u6309\u56E2\u961F --</option>'+cachedTeams.map(t=>'<option value="'+t.team_id+'"'+(selectedTeamId===t.team_id?' selected':'')+'>'+esc(t.name)+'</option>').join('')+'</select>'
2317
+ +(selectedTeamId?'<div class="hint" style="color:var(--accent)">&#128101; \u4F7F\u7528\u56E2\u961F\u5173\u7CFB\u4E0A\u4E0B\u6587</div>':'')+'</div>'
1494
2318
  +'<div class="fg"><label>\u9009\u62E9\u53C2\u4E0E\u7684\u78B3\u57FA\u8282\u70B9 <span style="color:var(--text-dim);font-weight:400">(\u9ED8\u8BA4\u9009\u4E2D\u7A7A\u95F2\u8282\u70B9)</span></label>'
1495
2319
  +'<div id="agent-chip-area">'+renderChips('all')+'</div></div>'
1496
2320
  +'<div class="btn-group">'
@@ -1511,7 +2335,9 @@ window.planWithAI=async function(){
1511
2335
  fc.innerHTML='<h3>AI \u667A\u80FD\u89C4\u5212</h3><div class="spinner-wrap"><div class="spinner"></div><div class="spinner-text">AI \u6B63\u5728\u5206\u6790\u9700\u6C42\u3001\u5339\u914D\u8282\u70B9\u3001\u751F\u6210\u8BDD\u672F...</div></div>';
1512
2336
 
1513
2337
  try{
1514
- const r=await fetch(API+'/jobs/plan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent_ids:Array.from(selectedAgentIds)})});
2338
+ const reqBody={prompt,agent_ids:Array.from(selectedAgentIds)};
2339
+ if(selectedTeamId)reqBody.team_id=selectedTeamId;
2340
+ const r=await fetch(API+'/jobs/plan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(reqBody)});
1515
2341
  const d=await r.json();
1516
2342
  if(!r.ok){
1517
2343
  toast(d.error||'\u89C4\u5212\u5931\u8D25',false);
@@ -1624,18 +2450,59 @@ window.reviewJob=async function(jobId){
1624
2450
  ov.innerHTML='<div class="form-card"><h3>AI \u805A\u5408\u5BA1\u67E5</h3><div class="spinner-wrap"><div class="spinner"></div><div class="spinner-text">AI \u6B63\u5728\u5BA1\u67E5\u6240\u6709\u4EA4\u4ED8\u7269...</div></div></div>';
1625
2451
  document.body.appendChild(ov);
1626
2452
  try{
1627
- const r=await fetch(API+'/jobs/'+jobId+'/review',{method:'POST'});
2453
+ const r=await fetch(API+'/jobs/'+jobId+'/review',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
1628
2454
  const d=await r.json();
1629
2455
  if(!r.ok){toast(d.error||'\u5BA1\u67E5\u5931\u8D25',false);ov.remove();return}
1630
2456
  const fc=ov.querySelector('.form-card');
1631
2457
  fc.innerHTML='<h3>AI \u805A\u5408\u5BA1\u67E5</h3>'
1632
2458
  +'<div style="font-size:12px;color:var(--text-dim);margin-bottom:12px">\u9700\u6C42: '+esc(d.original_prompt)+'</div>'
1633
- +'<div class="result-display" style="max-height:400px">'+esc(d.review).replace(/\\n/g,'<br>')+'</div>'
2459
+ +'<div class="md-editor"><div class="result-display" style="max-height:400px">'+esc(d.review).replace(/\\n/g,'<br>')+'</div></div>'
1634
2460
  +'<div style="font-size:10px;color:var(--text-dim);margin-top:8px;font-family:var(--font-mono)">\u5BA1\u67E5\u65F6\u95F4: '+new Date(d.reviewed_at).toLocaleString()+'</div>'
1635
2461
  +'<div class="btn-group"><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u5173\u95ED</button></div>';
1636
2462
  }catch(e){toast('\u7F51\u7EDC\u9519\u8BEF: '+e.message,false);ov.remove()}
1637
2463
  };
1638
2464
 
2465
+ window.showEvalDialog=function(jobId){
2466
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
2467
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
2468
+ ov.innerHTML='<div class="form-card"><h3>&#128202; \u751F\u6210\u7EE9\u6548\u8BC4\u4EF7</h3>'
2469
+ +'<div class="fg"><label>\u8BC4\u5206\u4F53\u7CFB</label><select id="eval-system">'
2470
+ +'<option value="ali">\u963F\u91CC\u7EE9\u6548 (3.25 / 3.5 / 3.5+ / 3.75 / 4.0)</option>'
2471
+ +'<option value="letter">SABCD \u7B49\u7EA7 (S / A / B / C / D)</option>'
2472
+ +'<option value="em">EM/MM \u4F53\u7CFB (EM+ / EM / MM+ / MM / MM-)</option>'
2473
+ +'</select></div>'
2474
+ +'<div class="btn-group"><button class="btn btn-primary" onclick="generateEval(\\''+jobId+'\\')">\u751F\u6210\u8BC4\u4EF7</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div></div>';
2475
+ document.body.appendChild(ov);
2476
+ };
2477
+ window.generateEval=async function(jobId){
2478
+ const ratingSystem=document.getElementById('eval-system').value;
2479
+ const ov=document.getElementById('overlay');
2480
+ const fc=ov.querySelector('.form-card');
2481
+ fc.innerHTML='<h3>&#128202; \u751F\u6210\u7EE9\u6548\u8BC4\u4EF7</h3><div class="spinner-wrap"><div class="spinner"></div><div class="spinner-text">AI \u6B63\u5728\u8BC4\u4F30\u6BCF\u4E2A\u78B3\u57FA\u8282\u70B9\u7684\u8868\u73B0...</div></div>';
2482
+ try{
2483
+ const r=await fetch(API+'/evaluations/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({job_id:jobId,rating_system:ratingSystem})});
2484
+ const d=await r.json();
2485
+ if(!r.ok){toast(d.error||'\u8BC4\u4EF7\u751F\u6210\u5931\u8D25',false);ov.remove();return}
2486
+ let h='<h3>&#128202; \u7EE9\u6548\u8BC4\u4EF7\u7ED3\u679C</h3>';
2487
+ h+='<div style="font-size:11px;color:var(--text-dim);margin-bottom:16px;font-family:var(--font-mono)">\u751F\u6210\u65F6\u95F4: '+new Date(d.generated_at).toLocaleString()+'</div>';
2488
+ const ratingTiers={ali:['4.0','3.75'],letter:['S','A'],em:['EM+','EM']};
2489
+ const lowTiers={ali:['3.25'],letter:['D'],em:['MM-']};
2490
+ for(const ev of d.evaluations){
2491
+ const agent=cachedAgents.find(a=>a.agent_id===ev.agent_id);
2492
+ const isTop=(ratingTiers[ratingSystem]||[]).includes(ev.rating);
2493
+ const isLow=(lowTiers[ratingSystem]||[]).includes(ev.rating);
2494
+ const tier=isTop?'top':isLow?'low':'mid';
2495
+ h+='<div style="padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:8px;margin-bottom:8px">';
2496
+ h+='<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px"><span style="font-weight:600;font-size:13px">'+esc(agent?agent.name:ev.agent_id)+'</span><span class="eval-badge '+tier+'">'+esc(ev.rating)+'</span></div>';
2497
+ h+='<div style="font-size:12px;color:var(--text-dim)">'+esc(ev.comment)+'</div>';
2498
+ h+='<div style="font-size:10px;color:var(--text-dim);margin-top:4px;font-family:var(--font-mono)">\u4EFB\u52A1: '+ev.trace_id+'</div>';
2499
+ h+='</div>';
2500
+ }
2501
+ h+='<div class="btn-group"><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u5173\u95ED</button></div>';
2502
+ fc.innerHTML=h;
2503
+ }catch(e){toast('\u7F51\u7EDC\u9519\u8BEF: '+e.message,false);ov.remove()}
2504
+ };
2505
+
1639
2506
  // \u2500\u2500\u2500 Task Detail Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1640
2507
  window.showTaskDetail=function(traceId){
1641
2508
  const t=allTasks.find(x=>x.trace_id===traceId);
@@ -1656,18 +2523,17 @@ window.showTaskDetail=function(traceId){
1656
2523
  h+='<div class="detail-row"><div class="detail-label">\u622A\u6B62\u65F6\u95F4</div><div class="detail-value" style="font-family:var(--font-mono)">'+new Date(t.deadline).toLocaleString()+'</div></div>';
1657
2524
 
1658
2525
  if(t.status==='RESOLVED'&&t.result_data){
1659
- // Show result
2526
+ // Show result + reject option (manager can reject after reviewing)
1660
2527
  let resultText='';
1661
2528
  if(typeof t.result_data==='object'&&t.result_data){resultText=t.result_data.text||JSON.stringify(t.result_data,null,2)}else{resultText=String(t.result_data||'')}
1662
- h+='<div class="detail-row"><div class="detail-label">\u4EA4\u4ED8\u7ED3\u679C</div><div class="result-display">'+esc(resultText)+'</div></div>';
1663
- h+='<div class="btn-group"><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u5173\u95ED</button></div>';
2529
+ h+='<div class="detail-row"><div class="detail-label">\u4EA4\u4ED8\u7ED3\u679C</div><div class="md-editor"><div class="result-display">'+esc(resultText)+'</div></div></div>';
2530
+ h+='<div class="btn-group"><button class="btn btn-danger btn-sm" onclick="taskReject(\\''+t.trace_id+'\\')">\u6253\u56DE\u91CD\u505A (Reject)</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u5173\u95ED</button></div>';
1664
2531
  }else{
1665
- // Input for resume/reject
1666
- h+='<div class="fg" style="margin-top:16px"><label>\u63D0\u4EA4 Human \u4EA4\u4ED8\u7269</label><textarea id="td-payload" rows="4" placeholder="\u7C98\u8D34\u4EA4\u4ED8\u7269\u5185\u5BB9\u3001GitHub PR/Commit URL\u3001\u5DE5\u4F5C\u6C47\u62A5\u7B49..."></textarea><div class="hint">\u652F\u6301\u8D34 GitHub URL\uFF08PR\u3001Commit\u3001Issue\uFF09\uFF0CAI \u5BA1\u67E5\u65F6\u4F1A\u5206\u6790</div></div>';
2532
+ // Input for resume (no reject for unsubmitted tasks)
2533
+ h+='<div class="fg" style="margin-top:16px"><label>\u63D0\u4EA4 Human \u4EA4\u4ED8\u7269</label><div class="md-editor"><textarea id="td-payload" rows="4" placeholder="\u7C98\u8D34\u4EA4\u4ED8\u7269\u5185\u5BB9\u3001GitHub PR/Commit URL\u3001\u5DE5\u4F5C\u6C47\u62A5\u7B49..."></textarea><button class="md-expand-btn" onclick="toggleFullscreen(this)" title="\u5C55\u5F00/\u6536\u8D77">&#x26F6;</button></div><div class="hint">\u652F\u6301\u8D34 GitHub URL\uFF08PR\u3001Commit\u3001Issue\uFF09\uFF0CAI \u5BA1\u67E5\u65F6\u4F1A\u5206\u6790</div></div>';
1667
2534
  h+='<div class="btn-group">';
1668
2535
  h+='<button class="btn btn-primary btn-sm" onclick="simulateDelivery(\\''+t.trace_id+'\\')">&#129302; \u6A21\u62DF\u4EA4\u4ED8</button>';
1669
2536
  h+='<button class="btn btn-green" onclick="taskResume(\\''+t.trace_id+'\\')">\u63D0\u4EA4\u4EA4\u4ED8 (Resume)</button>';
1670
- h+='<button class="btn btn-danger" onclick="taskReject(\\''+t.trace_id+'\\')">\u6253\u56DE\u91CD\u505A (Reject)</button>';
1671
2537
  h+='<button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button>';
1672
2538
  h+='</div>';
1673
2539
  }
@@ -1723,7 +2589,7 @@ function loadTerminal(el){
1723
2589
  el.innerHTML='<div class="section-hd"><h2>I/O \u4EA4\u4ED8\u7EC8\u7AEF</h2></div>'
1724
2590
  +'<div class="form-card" style="margin-top:12px"><h3>> \u63D0\u4EA4\u78B3\u57FA\u8282\u70B9\u4EA7\u51FA</h3>'
1725
2591
  +'<div class="fg"><label>Trace ID (\u8FFD\u8E2A\u7801)</label><input id="t-tid" placeholder="\u4F8B: TK-9527"/></div>'
1726
- +'<div class="fg"><label>\u4EA4\u4ED8\u8F7D\u8377</label><textarea id="t-payload" placeholder="\u7C98\u8D34\u4EA4\u4ED8\u7269\u5185\u5BB9\u3001GitHub PR/Commit URL\u3001\u5DE5\u4F5C\u6C47\u62A5\u7B49..."></textarea><div class="hint">\u652F\u6301\u8D34 GitHub URL\uFF08PR\u3001Commit\u3001Issue\uFF09\uFF0CAI \u5BA1\u67E5\u65F6\u4F1A\u5206\u6790</div></div>'
2592
+ +'<div class="fg"><label>\u4EA4\u4ED8\u8F7D\u8377</label><div class="md-editor"><textarea id="t-payload" placeholder="\u7C98\u8D34\u4EA4\u4ED8\u7269\u5185\u5BB9\u3001GitHub PR/Commit URL\u3001\u5DE5\u4F5C\u6C47\u62A5\u7B49..."></textarea><button class="md-expand-btn" onclick="toggleFullscreen(this)" title="\u5C55\u5F00/\u6536\u8D77">&#x26F6;</button></div><div class="hint">\u652F\u6301\u8D34 GitHub URL\uFF08PR\u3001Commit\u3001Issue\uFF09\uFF0CAI \u5BA1\u67E5\u65F6\u4F1A\u5206\u6790</div></div>'
1727
2593
  +'<div class="btn-group"><button class="btn btn-primary" onclick="doResume()">\u63D0\u4EA4\u5E76\u6062\u590D (Resume)</button><button class="btn btn-danger" onclick="doReject()">\u6253\u56DE\u91CD\u505A (Reject)</button></div></div>';
1728
2594
  }
1729
2595
  window.doResume=async function(){
@@ -1772,8 +2638,8 @@ window.showSettings=async function(){
1772
2638
  statusHtml='<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:12px;color:var(--red)">&#9888; \u672A\u914D\u7F6E API Key\uFF0CAI \u89C4\u5212\u529F\u80FD\u4E0D\u53EF\u7528</div>';
1773
2639
  }
1774
2640
  fc.innerHTML='<h3>LLM \u8BBE\u7F6E</h3>'+statusHtml
1775
- +'<div class="fg"><label>\u63D0\u4F9B\u5546</label><select id="cfg-provider"><option value="claude"'+(cfg.provider==='claude'?' selected':'')+'>Claude (Anthropic)</option><option value="openai"'+(cfg.provider==='openai'?' selected':'')+'>OpenAI</option></select></div>'
1776
- +'<div class="fg"><label>API Key</label><input id="cfg-key" type="password" placeholder="'+(cfg.api_key_set?'\u5DF2\u914D\u7F6E\uFF0C\u7559\u7A7A\u5219\u4E0D\u4FEE\u6539':'\u8F93\u5165\u4F60\u7684 API Key...')+'"/><div class="hint">Claude: sk-ant-... | OpenAI: sk-...</div></div>'
2641
+ +'<div class="fg"><label>API \u683C\u5F0F</label><select id="cfg-provider"><option value="anthropic"'+(cfg.provider==='anthropic'||cfg.provider==='claude'?' selected':'')+'>Anthropic Messages API</option><option value="openai"'+(cfg.provider==='openai'?' selected':'')+'>OpenAI Chat Completions API</option><option value="responses"'+(cfg.provider==='responses'?' selected':'')+'>OpenAI Responses API</option></select><div class="hint">\u9009\u62E9 API \u683C\u5F0F\u3002\u81EA\u5B9A\u4E49 Base URL \u53EF\u8FDE\u63A5\u4EFB\u4F55\u517C\u5BB9\u670D\u52A1\u3002</div></div>'
2642
+ +'<div class="fg"><label>API Key</label><input id="cfg-key" type="password" placeholder="'+(cfg.api_key_set?'\u5DF2\u914D\u7F6E\uFF0C\u7559\u7A7A\u5219\u4E0D\u4FEE\u6539':'\u8F93\u5165\u4F60\u7684 API Key...')+'"/><div class="hint">Anthropic: sk-ant-... | OpenAI: sk-...</div></div>'
1777
2643
  +'<div class="fg"><label>\u6A21\u578B <span style="color:var(--text-dim);font-weight:400">(\u53EF\u9009\uFF0C\u7559\u7A7A\u7528\u9ED8\u8BA4)</span></label><input id="cfg-model" value="'+esc(cfg.model||'')+'" placeholder="\u4F8B: claude-sonnet-4-20250514 / gpt-4o"/></div>'
1778
2644
  +'<div class="fg"><label>API Base URL <span style="color:var(--text-dim);font-weight:400">(\u53EF\u9009\uFF0C\u7559\u7A7A\u7528\u5B98\u65B9\u5730\u5740)</span></label><input id="cfg-baseurl" value="'+esc(cfg.base_url||'')+'" placeholder="\u4F8B: https://your-proxy.com"/><div class="hint">\u79C1\u6709\u90E8\u7F72: \u586B\u5199\u4F60\u7684\u6A21\u578B\u670D\u52A1\u5730\u5740\uFF0C\u5982 vLLM / Ollama / Azure \u7B49</div></div>'
1779
2645
  +'<div class="btn-group"><button class="btn btn-primary" onclick="saveSettings()">\u4FDD\u5B58</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div>';
@@ -1839,6 +2705,8 @@ function createServer(port = 2026) {
1839
2705
  app.use("/api/v1/tasks", tasks_default);
1840
2706
  app.use("/api/v1/jobs", sync_default);
1841
2707
  app.use("/api/v1/config", config_default);
2708
+ app.use("/api/v1/teams", teams_default);
2709
+ app.use("/api/v1/evaluations", evaluations_default);
1842
2710
  const dashboardHtml = getDashboardHtml();
1843
2711
  app.get("/", (_req, res) => {
1844
2712
  res.type("html").send(dashboardHtml);