@humanclaw/humanclaw 1.0.1 → 1.1.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
@@ -368,6 +368,246 @@ function dispatchJob(request, db2) {
368
368
  return { ...job, tasks };
369
369
  }
370
370
 
371
+ // src/llm/claude.ts
372
+ var ClaudeProvider = class {
373
+ apiKey;
374
+ model;
375
+ constructor(apiKey, model) {
376
+ this.apiKey = apiKey;
377
+ this.model = model || "claude-sonnet-4-20250514";
378
+ }
379
+ async complete(request) {
380
+ const systemMessages = request.messages.filter((m) => m.role === "system");
381
+ const nonSystemMessages = request.messages.filter((m) => m.role !== "system");
382
+ const body = {
383
+ model: this.model,
384
+ max_tokens: request.max_tokens || 4096,
385
+ messages: nonSystemMessages.map((m) => ({ role: m.role, content: m.content }))
386
+ };
387
+ if (request.temperature !== void 0) {
388
+ body.temperature = request.temperature;
389
+ }
390
+ if (systemMessages.length > 0) {
391
+ body.system = systemMessages.map((m) => m.content).join("\n\n");
392
+ }
393
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
394
+ method: "POST",
395
+ headers: {
396
+ "Content-Type": "application/json",
397
+ "x-api-key": this.apiKey,
398
+ "anthropic-version": "2023-06-01"
399
+ },
400
+ body: JSON.stringify(body)
401
+ });
402
+ if (!response.ok) {
403
+ const errorText = await response.text();
404
+ throw new Error(`Claude API error (${response.status}): ${errorText}`);
405
+ }
406
+ const data = await response.json();
407
+ const textBlock = data.content.find((c) => c.type === "text");
408
+ if (!textBlock) {
409
+ throw new Error("Claude API returned no text content");
410
+ }
411
+ return { content: textBlock.text };
412
+ }
413
+ };
414
+
415
+ // src/llm/openai.ts
416
+ var OpenAIProvider = class {
417
+ apiKey;
418
+ model;
419
+ constructor(apiKey, model) {
420
+ this.apiKey = apiKey;
421
+ this.model = model || "gpt-4o";
422
+ }
423
+ async complete(request) {
424
+ const body = {
425
+ model: this.model,
426
+ max_tokens: request.max_tokens || 4096,
427
+ messages: request.messages.map((m) => ({ role: m.role, content: m.content }))
428
+ };
429
+ if (request.temperature !== void 0) {
430
+ body.temperature = request.temperature;
431
+ }
432
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
433
+ method: "POST",
434
+ headers: {
435
+ "Content-Type": "application/json",
436
+ "Authorization": `Bearer ${this.apiKey}`
437
+ },
438
+ body: JSON.stringify(body)
439
+ });
440
+ if (!response.ok) {
441
+ const errorText = await response.text();
442
+ throw new Error(`OpenAI API error (${response.status}): ${errorText}`);
443
+ }
444
+ const data = await response.json();
445
+ const content = data.choices[0]?.message?.content;
446
+ if (!content) {
447
+ throw new Error("OpenAI API returned no content");
448
+ }
449
+ return { content };
450
+ }
451
+ };
452
+
453
+ // src/llm/index.ts
454
+ function getLlmConfig() {
455
+ const provider = process.env.HUMANCLAW_LLM_PROVIDER || "claude";
456
+ const apiKey = process.env.HUMANCLAW_LLM_API_KEY || "";
457
+ const model = process.env.HUMANCLAW_LLM_MODEL;
458
+ if (!apiKey) {
459
+ throw new Error(
460
+ "\u672A\u914D\u7F6E LLM API Key\u3002\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF HUMANCLAW_LLM_API_KEY\u3002\n\u4F8B\u5982: export HUMANCLAW_LLM_API_KEY=sk-..."
461
+ );
462
+ }
463
+ if (provider !== "claude" && provider !== "openai") {
464
+ throw new Error(`\u4E0D\u652F\u6301\u7684 LLM \u63D0\u4F9B\u5546: ${provider}\u3002\u652F\u6301: claude, openai`);
465
+ }
466
+ return { provider, apiKey, model };
467
+ }
468
+ function createLlmProvider(config) {
469
+ const cfg = config || getLlmConfig();
470
+ switch (cfg.provider) {
471
+ case "claude":
472
+ return new ClaudeProvider(cfg.apiKey, cfg.model);
473
+ case "openai":
474
+ return new OpenAIProvider(cfg.apiKey, cfg.model);
475
+ }
476
+ }
477
+
478
+ // src/services/planner.ts
479
+ function buildSystemPrompt() {
480
+ return `\u4F60\u662F HumanClaw \u4EFB\u52A1\u7F16\u6392\u89C4\u5212\u5668\u3002\u4F60\u7684\u5DE5\u4F5C\u662F\u5C06\u7528\u6237\u7684\u9700\u6C42\u62C6\u89E3\u4E3A\u53EF\u4EE5\u5206\u53D1\u7ED9\u7269\u7406\u8282\u70B9\uFF08\u771F\u5B9E\u4EBA\u7C7B\uFF09\u6267\u884C\u7684\u72EC\u7ACB\u5B50\u4EFB\u52A1\u3002
481
+
482
+ \u89C4\u5219\uFF1A
483
+ 1. \u5C06\u9700\u6C42\u62C6\u89E3\u4E3A\u6241\u5E73\u7684\u3001\u65E0\u4F9D\u8D56\u7684\u5B50\u4EFB\u52A1\u5217\u8868\uFF08\u6BCF\u4E2A\u4EFB\u52A1\u53EF\u4EE5\u72EC\u7ACB\u6267\u884C\uFF09
484
+ 2. \u6839\u636E\u6BCF\u4E2A Agent \u7684\u6280\u80FD\uFF08capabilities\uFF09\u548C\u5F53\u524D\u8D1F\u8F7D\u6765\u5339\u914D\u5206\u914D
485
+ 3. \u4E3A\u6BCF\u4E2A\u4EFB\u52A1\u751F\u6210\u4E00\u6BB5\u300C\u8BDD\u672F\u300D\u2014\u2014 \u8FD9\u662F\u76F4\u63A5\u53D1\u7ED9\u8BE5\u4EBA\u7C7B\u6267\u884C\u8005\u7684\u4EFB\u52A1\u8BF4\u660E\uFF0C\u8BED\u6C14\u81EA\u7136\u3001\u6E05\u6670\u3001\u4E13\u4E1A\uFF0C\u5305\u542B\u5177\u4F53\u7684\u4EA4\u4ED8\u7269\u8981\u6C42
486
+ 4. \u6839\u636E\u4EFB\u52A1\u590D\u6742\u5EA6\u8BBE\u7F6E\u5408\u7406\u7684\u622A\u6B62\u65F6\u95F4\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u65F6\u95F4\uFF09
487
+ 5. \u6BCF\u4E2A Agent \u6700\u591A\u5206\u914D\u4E00\u4E2A\u4EFB\u52A1\uFF08\u9664\u975E\u4EBA\u624B\u4E0D\u591F\uFF09
488
+
489
+ \u4F60\u5FC5\u987B\u4E25\u683C\u8F93\u51FA\u4EE5\u4E0B JSON \u683C\u5F0F\uFF08\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u5176\u4ED6\u5185\u5BB9\uFF09\uFF1A
490
+
491
+ \`\`\`json
492
+ [
493
+ {
494
+ "assignee_id": "agent\u7684ID",
495
+ "assignee_name": "agent\u7684\u540D\u5B57",
496
+ "todo_description": "\u7B80\u77ED\u7684\u4EFB\u52A1\u6807\u9898/\u63CF\u8FF0",
497
+ "briefing": "\u8BDD\u672F\uFF1A\u8BE6\u7EC6\u7684\u4EFB\u52A1\u8BF4\u660E\uFF0C\u5305\u62EC\u76EE\u6807\u3001\u4EA4\u4ED8\u7269\u8981\u6C42\u3001\u6CE8\u610F\u4E8B\u9879\u3002\u8BED\u6C14\u50CF\u4E00\u4E2A\u9760\u8C31\u7684\u9879\u76EE\u7ECF\u7406\u5728\u7ED9\u7EC4\u5458\u5206\u914D\u4EFB\u52A1\u3002",
498
+ "deadline": "ISO 8601 \u65F6\u95F4"
499
+ }
500
+ ]
501
+ \`\`\``;
502
+ }
503
+ function buildUserPrompt(prompt, agents) {
504
+ const now = /* @__PURE__ */ new Date();
505
+ const agentList = agents.map((a) => {
506
+ 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";
507
+ const speed = a.avg_delivery_hours !== null ? `\u5E73\u5747\u4EA4\u4ED8\u65F6\u95F4 ${a.avg_delivery_hours}h` : "\u6682\u65E0\u5386\u53F2\u6570\u636E";
508
+ return `- ${a.name} (ID: ${a.agent_id}) \u6280\u80FD: [${a.capabilities.join(", ")}] ${load} ${speed}`;
509
+ }).join("\n");
510
+ return `\u5F53\u524D\u65F6\u95F4: ${now.toISOString()}
511
+
512
+ \u53EF\u7528\u7684\u7269\u7406\u8282\u70B9\uFF1A
513
+ ${agentList}
514
+
515
+ \u9700\u6C42\uFF1A
516
+ ${prompt}
517
+
518
+ \u8BF7\u6839\u636E\u4EE5\u4E0A\u4FE1\u606F\u62C6\u89E3\u4EFB\u52A1\u5E76\u5206\u914D\u3002\u8F93\u51FA JSON \u6570\u7EC4\u3002`;
519
+ }
520
+ function extractJson(raw) {
521
+ const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
522
+ if (codeBlockMatch) {
523
+ return codeBlockMatch[1].trim();
524
+ }
525
+ const arrayMatch = raw.match(/\[[\s\S]*\]/);
526
+ if (arrayMatch) {
527
+ return arrayMatch[0];
528
+ }
529
+ return raw.trim();
530
+ }
531
+ function validatePlannedTasks(parsed, validAgentIds, agents) {
532
+ if (!Array.isArray(parsed)) {
533
+ throw new Error("LLM \u8FD4\u56DE\u7684\u4E0D\u662F\u6709\u6548\u7684\u4EFB\u52A1\u6570\u7EC4");
534
+ }
535
+ if (parsed.length === 0) {
536
+ throw new Error("LLM \u672A\u751F\u6210\u4EFB\u4F55\u4EFB\u52A1");
537
+ }
538
+ return parsed.map((item, i) => {
539
+ if (!item.todo_description || typeof item.todo_description !== "string") {
540
+ throw new Error(`\u4EFB\u52A1 ${i + 1} \u7F3A\u5C11 todo_description`);
541
+ }
542
+ if (!item.briefing || typeof item.briefing !== "string") {
543
+ throw new Error(`\u4EFB\u52A1 ${i + 1} \u7F3A\u5C11 briefing (\u8BDD\u672F)`);
544
+ }
545
+ let assigneeId = String(item.assignee_id || "");
546
+ let assigneeName = String(item.assignee_name || "");
547
+ if (!validAgentIds.has(assigneeId)) {
548
+ const fallback = agents[0];
549
+ if (fallback) {
550
+ assigneeId = fallback.agent_id;
551
+ assigneeName = fallback.name;
552
+ }
553
+ }
554
+ let deadline = String(item.deadline || "");
555
+ if (!deadline || isNaN(Date.parse(deadline))) {
556
+ deadline = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
557
+ }
558
+ return {
559
+ assignee_id: assigneeId,
560
+ assignee_name: assigneeName,
561
+ todo_description: String(item.todo_description),
562
+ briefing: String(item.briefing),
563
+ deadline
564
+ };
565
+ });
566
+ }
567
+ async function planJob(request, provider, db2) {
568
+ const conn = db2 ?? getDb();
569
+ const allAgents = listAgentsWithMetrics(conn);
570
+ let agents;
571
+ if (request.agent_ids && request.agent_ids.length > 0) {
572
+ agents = allAgents.filter((a) => request.agent_ids.includes(a.agent_id));
573
+ if (agents.length === 0) {
574
+ throw new Error("\u6307\u5B9A\u7684 Agent \u5747\u4E0D\u5B58\u5728");
575
+ }
576
+ } else {
577
+ agents = allAgents.filter((a) => a.status === "IDLE");
578
+ if (agents.length === 0) {
579
+ agents = allAgents.filter((a) => a.status !== "OFFLINE" && a.status !== "OOM");
580
+ }
581
+ if (agents.length === 0) {
582
+ throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u7269\u7406\u8282\u70B9\u3002\u8BF7\u5148\u5728\u78B3\u57FA\u7B97\u529B\u6C60\u4E2D\u6DFB\u52A0\u8282\u70B9\u3002");
583
+ }
584
+ }
585
+ const llm = provider ?? createLlmProvider();
586
+ const systemPrompt = buildSystemPrompt();
587
+ const userPrompt = buildUserPrompt(request.prompt, agents);
588
+ const response = await llm.complete({
589
+ messages: [
590
+ { role: "system", content: systemPrompt },
591
+ { role: "user", content: userPrompt }
592
+ ],
593
+ temperature: 0.3,
594
+ max_tokens: 4096
595
+ });
596
+ const jsonStr = extractJson(response.content);
597
+ let parsed;
598
+ try {
599
+ parsed = JSON.parse(jsonStr);
600
+ } catch {
601
+ throw new Error("AI \u8FD4\u56DE\u7684\u5185\u5BB9\u65E0\u6CD5\u89E3\u6790\u4E3A JSON\uFF0C\u8BF7\u91CD\u8BD5");
602
+ }
603
+ const validIds = new Set(agents.map((a) => a.agent_id));
604
+ const plannedTasks = validatePlannedTasks(parsed, validIds, agents);
605
+ return {
606
+ original_prompt: request.prompt,
607
+ planned_tasks: plannedTasks
608
+ };
609
+ }
610
+
371
611
  // src/routes/jobs.ts
372
612
  var router2 = Router2();
373
613
  router2.post("/create", (req, res) => {
@@ -399,6 +639,21 @@ router2.get("/active", (_req, res) => {
399
639
  const jobs = listActiveJobs();
400
640
  res.json({ jobs });
401
641
  });
642
+ router2.post("/plan", async (req, res) => {
643
+ const body = req.body;
644
+ if (!body.prompt || typeof body.prompt !== "string") {
645
+ res.status(400).json({ error: "prompt is required" });
646
+ return;
647
+ }
648
+ try {
649
+ const plan = await planJob(body);
650
+ res.json(plan);
651
+ } catch (error) {
652
+ const message = error instanceof Error ? error.message : "Unknown error";
653
+ const status = message.includes("API Key") || message.includes("API key") ? 503 : 400;
654
+ res.status(status).json({ error: message });
655
+ }
656
+ });
402
657
  router2.get("/:job_id", (req, res) => {
403
658
  const { job_id } = req.params;
404
659
  const job = getJobWithTasks(job_id);
@@ -572,133 +827,254 @@ function getDashboardHtml() {
572
827
  <head>
573
828
  <meta charset="UTF-8"/>
574
829
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
575
- <title>HumanClaw - Async Physical Node Orchestration</title>
830
+ <title>HumanClaw - \u5F02\u6B65\u7269\u7406\u8282\u70B9\u7F16\u6392</title>
576
831
  <style>
577
- :root{--bg:#0a0a0f;--surface:#14141f;--surface-hover:#1a1a2e;--border:#2a2a3e;--text:#e0e0e8;--text-dim:#8888a0;--accent:#00d4ff;--green:#22c55e;--yellow:#eab308;--red:#ef4444;--purple:#a855f7;--font-mono:'JetBrains Mono','Fira Code',monospace;--font-sans:'Inter',-apple-system,sans-serif}
832
+ :root{--bg:#0f1117;--surface:#1a1d27;--surface-hover:#242836;--border:#2e3346;--text:#e2e4ed;--text-dim:#7b8196;--accent:#00d4ff;--accent-dim:rgba(0,212,255,.12);--green:#22c55e;--yellow:#eab308;--red:#ef4444;--purple:#a855f7;--font-mono:'SF Mono','JetBrains Mono','Fira Code',monospace;--font-sans:-apple-system,'Inter','Segoe UI',sans-serif;--radius:10px}
578
833
  *{margin:0;padding:0;box-sizing:border-box}
579
- body{background:var(--bg);color:var(--text);font-family:var(--font-sans);min-height:100vh}
580
- header{padding:24px 32px 8px;border-bottom:1px solid var(--border)}
581
- header h1{font-family:var(--font-mono);font-size:24px;color:var(--accent);letter-spacing:2px}
582
- .subtitle{color:var(--text-dim);font-size:13px;margin-top:4px}
583
- nav{display:flex;gap:0;border-bottom:1px solid var(--border);padding:0 32px}
584
- .tab{background:none;border:none;color:var(--text-dim);padding:12px 20px;font-size:14px;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s;font-family:var(--font-sans)}
585
- .tab:hover{color:var(--text)}
834
+ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);min-height:100vh;line-height:1.5}
835
+ header{padding:20px 32px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px}
836
+ header h1{font-family:var(--font-mono);font-size:22px;color:var(--accent);letter-spacing:1.5px}
837
+ .subtitle{color:var(--text-dim);font-size:12px;border-left:1px solid var(--border);padding-left:14px}
838
+ nav{display:flex;border-bottom:1px solid var(--border);padding:0 32px;gap:4px}
839
+ .tab{background:none;border:none;color:var(--text-dim);padding:12px 18px;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;font-family:var(--font-sans);font-weight:500}
840
+ .tab:hover{color:var(--text);background:var(--surface)}
586
841
  .tab.active{color:var(--accent);border-bottom-color:var(--accent)}
587
- main{padding:24px 32px}
842
+ main{padding:24px 32px;max-width:1200px}
588
843
  .panel{display:none}.panel.active{display:block}
589
- .fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
590
- .agent-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;transition:border-color .2s}
591
- .agent-card:hover{border-color:var(--accent)}
592
- .agent-header{display:flex;align-items:center;gap:10px;margin-bottom:12px}
593
- .status-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
594
- .status-dot.IDLE{background:var(--green);box-shadow:0 0 8px var(--green)}
595
- .status-dot.BUSY{background:var(--yellow);box-shadow:0 0 8px var(--yellow)}
596
- .status-dot.OFFLINE{background:var(--red);box-shadow:0 0 8px var(--red)}
597
- .status-dot.OOM{background:var(--purple);box-shadow:0 0 8px var(--purple)}
598
- .agent-name{font-weight:600;font-size:16px}
599
- .agent-id{color:var(--text-dim);font-family:var(--font-mono);font-size:12px}
600
- .agent-caps{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
601
- .cap-tag{background:var(--surface-hover);border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:12px;color:var(--accent)}
602
- .agent-metrics{margin-top:12px;font-size:12px;color:var(--text-dim)}
603
- .job-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px;margin-bottom:16px}
604
- .job-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
605
- .job-title{font-weight:600;font-size:15px}
606
- .job-id{font-family:var(--font-mono);font-size:12px;color:var(--text-dim)}
607
- .progress-bar{background:var(--surface-hover);border-radius:4px;height:6px;margin:8px 0 16px;overflow:hidden}
608
- .progress-fill{background:var(--accent);height:100%;border-radius:4px;transition:width .3s ease}
609
- .progress-label{font-size:12px;color:var(--text-dim);margin-bottom:4px}
610
- .kanban{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
611
- .kanban-lane{min-height:60px}
612
- .lane-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;padding-bottom:4px;border-bottom:2px solid var(--border)}
613
- .lane-title.dispatched{color:var(--yellow);border-color:var(--yellow)}
614
- .lane-title.overdue{color:var(--red);border-color:var(--red)}
615
- .lane-title.resolved{color:var(--green);border-color:var(--green)}
616
- .task-card{background:var(--surface-hover);border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:8px;font-size:13px}
617
- .task-trace{font-family:var(--font-mono);font-size:11px;color:var(--accent);margin-bottom:4px}
618
- .task-desc{color:var(--text);margin-bottom:4px}
619
- .task-meta{font-size:11px;color:var(--text-dim)}
620
- .terminal-section{max-width:640px}
621
- .terminal-section h2{font-size:16px;margin-bottom:16px;color:var(--accent);font-family:var(--font-mono)}
622
- .form-group{margin-bottom:16px}
623
- .form-group label{display:block;font-size:13px;color:var(--text-dim);margin-bottom:6px}
624
- .form-group input,.form-group textarea{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:10px 14px;color:var(--text);font-size:14px;font-family:var(--font-mono);outline:none;transition:border-color .2s}
625
- .form-group input:focus,.form-group textarea:focus{border-color:var(--accent)}
626
- .form-group textarea{min-height:120px;resize:vertical}
627
- .btn-group{display:flex;gap:12px;margin-top:20px}
628
- .btn{padding:10px 24px;border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:opacity .2s}
629
- .btn:hover{opacity:.85}
630
- .btn:disabled{opacity:.5;cursor:not-allowed}
844
+ /* Cards */
845
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;transition:border-color .15s}
846
+ .card:hover{border-color:color-mix(in srgb,var(--accent) 40%,transparent)}
847
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;margin-top:16px}
848
+ /* Agent Card */
849
+ .agent-header{display:flex;align-items:center;gap:10px;margin-bottom:10px}
850
+ .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
851
+ .dot.IDLE{background:var(--green);box-shadow:0 0 8px var(--green)}.dot.BUSY{background:var(--yellow);box-shadow:0 0 8px var(--yellow)}.dot.OFFLINE{background:var(--red);box-shadow:0 0 6px var(--red)}.dot.OOM{background:var(--purple);box-shadow:0 0 8px var(--purple)}
852
+ .agent-name{font-weight:600;font-size:15px}
853
+ .agent-id{color:var(--text-dim);font-family:var(--font-mono);font-size:11px;margin-bottom:8px}
854
+ .caps{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
855
+ .cap{background:var(--accent-dim);border:1px solid color-mix(in srgb,var(--accent) 25%,transparent);border-radius:4px;padding:1px 8px;font-size:11px;color:var(--accent)}
856
+ .agent-meta{margin-top:10px;font-size:11px;color:var(--text-dim);display:flex;gap:12px}
857
+ .agent-actions{margin-top:12px;display:flex;gap:6px}
858
+ .agent-actions select{background:var(--surface-hover);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11px;cursor:pointer;outline:none}
859
+ .agent-actions button{background:var(--red);color:#fff;border:none;border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer}
860
+ .agent-actions button:hover{opacity:.8}
861
+ /* Stats Bar */
862
+ .stats{display:flex;gap:12px;flex-wrap:wrap}
863
+ .stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 22px;text-align:center;min-width:90px}
864
+ .stat-val{font-size:28px;font-weight:700;font-family:var(--font-mono)}
865
+ .stat-lbl{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
866
+ /* Forms */
867
+ .form-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-top:16px;max-width:620px}
868
+ .form-card h3{font-size:15px;color:var(--accent);font-family:var(--font-mono);margin-bottom:16px}
869
+ .fg{margin-bottom:14px}
870
+ .fg label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:5px;font-weight:500}
871
+ .fg input,.fg textarea,.fg select{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);font-size:13px;font-family:var(--font-sans);outline:none;transition:border-color .15s}
872
+ .fg input:focus,.fg textarea:focus,.fg select:focus{border-color:var(--accent)}
873
+ .fg textarea{min-height:80px;resize:vertical;font-family:var(--font-mono);font-size:12px}
874
+ .fg .hint{font-size:11px;color:var(--text-dim);margin-top:4px}
875
+ .btn{padding:10px 22px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
876
+ .btn:hover{opacity:.85;transform:translateY(-1px)}
877
+ .btn:active{transform:translateY(0)}
878
+ .btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
631
879
  .btn-primary{background:var(--accent);color:var(--bg)}
880
+ .btn-green{background:var(--green);color:#fff}
632
881
  .btn-danger{background:var(--red);color:#fff}
633
- .toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;font-size:14px;z-index:1000;animation:slide-in .3s ease}
634
- .toast.success{background:var(--green);color:#fff}
635
- .toast.error{background:var(--red);color:#fff}
636
- @keyframes slide-in{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
637
- .empty{color:var(--text-dim);text-align:center;padding:40px 0;font-size:14px}
638
- .fleet-summary{display:flex;gap:16px;margin-bottom:20px}
639
- .summary-item{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 20px;text-align:center}
640
- .summary-value{font-size:24px;font-weight:700;font-family:var(--font-mono)}
641
- .summary-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
882
+ .btn-ghost{background:transparent;color:var(--accent);border:1px solid var(--border)}
883
+ .btn-ghost:hover{background:var(--accent-dim)}
884
+ .btn-sm{padding:6px 14px;font-size:12px}
885
+ .btn-group{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
886
+ /* Section header */
887
+ .section-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
888
+ .section-hd h2{font-size:16px;font-weight:600}
889
+ /* Job card */
890
+ .job-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-top:14px}
891
+ .job-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
892
+ .job-title{font-weight:600;font-size:14px}
893
+ .job-id{font-family:var(--font-mono);font-size:11px;color:var(--text-dim)}
894
+ .pbar-wrap{margin:6px 0 14px}
895
+ .pbar-label{font-size:11px;color:var(--text-dim);margin-bottom:4px}
896
+ .pbar{background:var(--surface-hover);border-radius:4px;height:6px;overflow:hidden}
897
+ .pbar-fill{background:var(--accent);height:100%;border-radius:4px;transition:width .3s}
898
+ .kanban{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
899
+ .lane{min-height:40px}
900
+ .lane-hd{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;padding-bottom:4px;border-bottom:2px solid var(--border)}
901
+ .lane-hd.y{color:var(--yellow);border-color:var(--yellow)}.lane-hd.r{color:var(--red);border-color:var(--red)}.lane-hd.g{color:var(--green);border-color:var(--green)}
902
+ .tcard{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;margin-bottom:6px;font-size:12px}
903
+ .tcard-trace{font-family:var(--font-mono);font-size:10px;color:var(--accent);margin-bottom:3px}
904
+ .tcard-meta{font-size:10px;color:var(--text-dim);margin-top:3px}
905
+ /* Toast */
906
+ .toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;font-size:13px;z-index:9999;animation:slide-up .25s ease;font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.4)}
907
+ .toast.ok{background:var(--green);color:#fff}.toast.err{background:var(--red);color:#fff}
908
+ @keyframes slide-up{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
909
+ /* Empty state */
910
+ .empty-state{text-align:center;padding:48px 20px;color:var(--text-dim)}
911
+ .empty-state .icon{font-size:48px;margin-bottom:12px;opacity:.6}
912
+ .empty-state p{font-size:14px;margin-bottom:16px;max-width:360px;margin-left:auto;margin-right:auto;line-height:1.6}
913
+ /* Modal/Overlay */
914
+ .overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:100;display:flex;align-items:flex-start;justify-content:center;padding-top:60px;animation:fade-in .15s ease}
915
+ .overlay .form-card{max-width:620px;width:100%;max-height:85vh;overflow-y:auto;margin:0}
916
+ @keyframes fade-in{from{opacity:0}to{opacity:1}}
917
+ /* Agent chips for AI planning */
918
+ .chip-bar{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
919
+ .chip-filter{background:var(--surface-hover);color:var(--text-dim);border:1px solid var(--border);border-radius:14px;padding:3px 12px;font-size:11px;cursor:pointer;transition:all .15s}
920
+ .chip-filter.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent)}
921
+ .agent-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
922
+ .achip{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:12px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px;user-select:none}
923
+ .achip:hover{border-color:var(--text-dim)}
924
+ .achip.selected{border-color:var(--accent);background:var(--accent-dim)}
925
+ .achip .adot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
926
+ .achip .adot.IDLE{background:var(--green)}.achip .adot.BUSY{background:var(--yellow)}.achip .adot.OFFLINE{background:var(--red)}.achip .adot.OOM{background:var(--purple)}
927
+ /* Plan preview cards */
928
+ .plan-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
929
+ .plan-card-hd{display:flex;align-items:center;gap:8px;margin-bottom:8px}
930
+ .plan-card-hd .adot{width:8px;height:8px;border-radius:50%}
931
+ .plan-card-agent{font-weight:600;font-size:13px}
932
+ .plan-card-agentid{font-family:var(--font-mono);font-size:10px;color:var(--text-dim)}
933
+ .plan-card-desc{font-size:13px;margin-bottom:10px;font-weight:500}
934
+ .briefing-box{background:var(--surface);border-left:3px solid var(--accent);border-radius:0 6px 6px 0;padding:10px 14px;font-size:12px;line-height:1.6;color:var(--text);margin-bottom:8px;position:relative}
935
+ .briefing-label{font-size:10px;color:var(--accent);font-weight:600;margin-bottom:4px;font-family:var(--font-mono)}
936
+ .briefing-copy{position:absolute;top:8px;right:8px;background:var(--surface-hover);border:1px solid var(--border);color:var(--text-dim);border-radius:4px;padding:2px 8px;font-size:10px;cursor:pointer}
937
+ .briefing-copy:hover{color:var(--accent);border-color:var(--accent)}
938
+ .plan-card-dl{font-size:11px;color:var(--text-dim);font-family:var(--font-mono)}
939
+ /* Spinner */
940
+ .spinner-wrap{text-align:center;padding:40px 20px}
941
+ .spinner{display:inline-block;width:28px;height:28px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
942
+ @keyframes spin{to{transform:rotate(360deg)}}
943
+ .spinner-text{color:var(--text-dim);font-size:13px;margin-top:12px}
944
+ /* Manual task row */
945
+ .task-row{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:10px;position:relative}
946
+ .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}
947
+ .task-row .fg{margin-bottom:10px}
948
+ .task-row .fg:last-child{margin-bottom:0}
642
949
  </style>
643
950
  </head>
644
951
  <body>
645
952
  <header>
646
953
  <h1>HumanClaw</h1>
647
- <p class="subtitle">Async Physical Node Orchestration Framework</p>
954
+ <span class="subtitle">\u5F02\u6B65\u7269\u7406\u8282\u70B9\u7F16\u6392\u6846\u67B6</span>
648
955
  </header>
649
956
  <nav>
650
- <button class="tab active" data-panel="fleet">Carbon Compute Pool</button>
651
- <button class="tab" data-panel="pipeline">Task Pipeline</button>
652
- <button class="tab" data-panel="terminal">I/O Terminal</button>
957
+ <button class="tab active" data-panel="fleet">\u78B3\u57FA\u7B97\u529B\u6C60</button>
958
+ <button class="tab" data-panel="pipeline">\u7F16\u6392\u5927\u76D8</button>
959
+ <button class="tab" data-panel="terminal">I/O \u7EC8\u7AEF</button>
653
960
  </nav>
654
961
  <main>
655
962
  <section id="fleet" class="panel active"></section>
656
963
  <section id="pipeline" class="panel"></section>
657
964
  <section id="terminal" class="panel"></section>
658
965
  </main>
966
+
659
967
  <script>
660
968
  const API='/api/v1';
661
969
  function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
970
+ function toast(msg,ok){
971
+ document.querySelectorAll('.toast').forEach(t=>t.remove());
972
+ const t=document.createElement('div');t.className='toast '+(ok?'ok':'err');t.textContent=msg;document.body.appendChild(t);
973
+ setTimeout(()=>t.remove(),3500);
974
+ }
975
+ let cachedAgents=[];
976
+ let currentPlan=null;
977
+ let selectedAgentIds=new Set();
662
978
 
663
- // \u2500\u2500\u2500 Fleet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
979
+ // \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
980
+ // FLEET
981
+ // \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
664
982
  async function loadFleet(el){
665
- el.innerHTML='<div class="empty">Loading...</div>';
983
+ el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u4E2D...</p></div>';
666
984
  try{
667
985
  const r=await fetch(API+'/nodes/status');
668
986
  const d=await r.json();
669
- let h='<div class="fleet-summary">';
670
- h+='<div class="summary-item"><div class="summary-value">'+d.total+'</div><div class="summary-label">Total</div></div>';
671
- h+='<div class="summary-item"><div class="summary-value" style="color:var(--green)">'+d.idle+'</div><div class="summary-label">Idle</div></div>';
672
- h+='<div class="summary-item"><div class="summary-value" style="color:var(--yellow)">'+d.busy+'</div><div class="summary-label">Busy</div></div>';
673
- h+='<div class="summary-item"><div class="summary-value" style="color:var(--red)">'+d.offline+'</div><div class="summary-label">Offline</div></div>';
674
- h+='<div class="summary-item"><div class="summary-value" style="color:var(--purple)">'+d.oom+'</div><div class="summary-label">OOM</div></div>';
987
+ cachedAgents=d.agents||[];
988
+ let h='<div class="section-hd"><h2>\u78B3\u57FA\u7B97\u529B\u6C60</h2><button class="btn btn-primary btn-sm" onclick="showAddAgent()">+ \u6DFB\u52A0\u8282\u70B9</button></div>';
989
+ h+='<div class="stats">';
990
+ h+='<div class="stat"><div class="stat-val">'+d.total+'</div><div class="stat-lbl">\u603B\u8BA1</div></div>';
991
+ h+='<div class="stat"><div class="stat-val" style="color:var(--green)">'+d.idle+'</div><div class="stat-lbl">\u7A7A\u95F2</div></div>';
992
+ h+='<div class="stat"><div class="stat-val" style="color:var(--yellow)">'+d.busy+'</div><div class="stat-lbl">\u5FD9\u788C</div></div>';
993
+ h+='<div class="stat"><div class="stat-val" style="color:var(--red)">'+d.offline+'</div><div class="stat-lbl">\u79BB\u7EBF</div></div>';
994
+ h+='<div class="stat"><div class="stat-val" style="color:var(--purple)">'+d.oom+'</div><div class="stat-lbl">\u5D29\u6E83</div></div>';
675
995
  h+='</div>';
676
- if(!d.agents.length){el.innerHTML=h+'<div class="empty">No physical nodes registered. Use CLI: humanclaw agent add</div>';return}
677
- h+='<div class="fleet-grid">';
996
+ if(!d.agents.length){
997
+ h+='<div class="empty-state" style="margin-top:32px"><div class="icon">\u{1F464}</div><p>\u8FD8\u6CA1\u6709\u7269\u7406\u8282\u70B9\u3002\u70B9\u51FB\u4E0A\u65B9\u300C+ \u6DFB\u52A0\u8282\u70B9\u300D\u6CE8\u518C\u4F60\u7684\u7B2C\u4E00\u4E2A\u78B3\u57FA\u7B97\u529B\u5355\u5143\u3002</p></div>';
998
+ el.innerHTML=h;return;
999
+ }
1000
+ h+='<div class="grid">';
678
1001
  for(const a of d.agents){
679
- h+='<div class="agent-card"><div class="agent-header"><span class="status-dot '+a.status+'"></span><span class="agent-name">'+esc(a.name)+'</span></div>';
680
- h+='<div class="agent-id">'+a.agent_id+'</div><div class="agent-caps">';
681
- for(const c of a.capabilities)h+='<span class="cap-tag">'+esc(c)+'</span>';
682
- h+='</div><div class="agent-metrics">Active tasks: '+a.active_task_count;
683
- if(a.avg_delivery_hours!==null)h+=' | Avg delivery: '+a.avg_delivery_hours+'h';
1002
+ h+='<div class="card"><div class="agent-header"><span class="dot '+a.status+'"></span><span class="agent-name">'+esc(a.name)+'</span></div>';
1003
+ h+='<div class="agent-id">'+a.agent_id+'</div>';
1004
+ h+='<div class="caps">';for(const c of a.capabilities)h+='<span class="cap">'+esc(c)+'</span>';h+='</div>';
1005
+ h+='<div class="agent-meta"><span>\u4EFB\u52A1: '+a.active_task_count+'</span>';
1006
+ if(a.avg_delivery_hours!==null)h+='<span>\u5E73\u5747\u4EA4\u4ED8: '+a.avg_delivery_hours+'h</span>';
1007
+ h+='</div>';
1008
+ h+='<div class="agent-actions">';
1009
+ h+='<select onchange="changeStatus(\\''+a.agent_id+'\\',this.value)">';
1010
+ for(const s of ['IDLE','BUSY','OFFLINE','OOM'])h+='<option'+(s===a.status?' selected':'')+'>'+s+'</option>';
1011
+ h+='</select>';
1012
+ h+='<button onclick="deleteAgent(\\''+a.agent_id+'\\')">\u5220\u9664</button>';
684
1013
  h+='</div></div>';
685
1014
  }
686
1015
  h+='</div>';
687
1016
  el.innerHTML=h;
688
- }catch{el.innerHTML='<div class="empty">Failed to load fleet data</div>'}
1017
+ }catch(e){el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u5931\u8D25: '+e.message+'</p></div>'}
689
1018
  }
1019
+ window.showAddAgent=function(){
1020
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
1021
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
1022
+ ov.innerHTML='<div class="form-card"><h3>+ \u6DFB\u52A0\u7269\u7406\u8282\u70B9</h3>'
1023
+ +'<div class="fg"><label>\u8282\u70B9\u540D\u79F0</label><input id="aa-name" placeholder="\u4F8B: \u524D\u7AEF\u8001\u674E"/></div>'
1024
+ +'<div class="fg"><label>\u6280\u80FD\u6807\u7B7E</label><input id="aa-caps" placeholder="\u4F8B: UI/UX, \u524D\u7AEF\u5F00\u53D1, \u6297\u538B\u80FD\u529B\u5F3A"/><div class="hint">\u591A\u4E2A\u6807\u7B7E\u7528\u9017\u53F7\u5206\u9694</div></div>'
1025
+ +'<div class="btn-group"><button class="btn btn-primary" onclick="submitAgent()">\u6CE8\u518C\u8282\u70B9</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div></div>';
1026
+ document.body.appendChild(ov);
1027
+ document.getElementById('aa-name').focus();
1028
+ };
1029
+ window.submitAgent=async function(){
1030
+ const name=document.getElementById('aa-name').value.trim();
1031
+ const caps=document.getElementById('aa-caps').value.trim();
1032
+ if(!name){toast('\u8BF7\u8F93\u5165\u8282\u70B9\u540D\u79F0',false);return}
1033
+ if(!caps){toast('\u8BF7\u8F93\u5165\u81F3\u5C11\u4E00\u4E2A\u6280\u80FD\u6807\u7B7E',false);return}
1034
+ try{
1035
+ const r=await fetch(API+'/nodes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,capabilities:caps.split(',').map(s=>s.trim()).filter(Boolean)})});
1036
+ const d=await r.json();
1037
+ if(!r.ok){toast(d.error||'\u6CE8\u518C\u5931\u8D25',false);return}
1038
+ toast('\u8282\u70B9 '+d.agent_id+' \u6CE8\u518C\u6210\u529F\uFF01',true);
1039
+ document.getElementById('overlay').remove();
1040
+ load('fleet');
1041
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1042
+ };
1043
+ window.changeStatus=async function(id,status){
1044
+ try{
1045
+ const r=await fetch(API+'/nodes/'+id+'/status',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({status})});
1046
+ if(!r.ok){const d=await r.json();toast(d.error||'\u66F4\u65B0\u5931\u8D25',false);return}
1047
+ toast('\u72B6\u6001\u5DF2\u66F4\u65B0',true);
1048
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1049
+ };
1050
+ window.deleteAgent=async function(id){
1051
+ if(!confirm('\u786E\u5B9A\u5220\u9664\u8282\u70B9 '+id+'\uFF1F'))return;
1052
+ try{
1053
+ const r=await fetch(API+'/nodes/'+id,{method:'DELETE'});
1054
+ if(!r.ok){const d=await r.json();toast(d.error||'\u5220\u9664\u5931\u8D25',false);return}
1055
+ toast('\u8282\u70B9\u5DF2\u5220\u9664',true);load('fleet');
1056
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1057
+ };
690
1058
 
691
- // \u2500\u2500\u2500 Pipeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
692
- function taskCard(t){
693
- return '<div class="task-card"><div class="task-trace">'+t.trace_id+'</div><div class="task-desc">'+esc(t.todo_description)+'</div><div class="task-meta">Assignee: '+t.assignee_id+' | Deadline: '+new Date(t.deadline).toLocaleString()+'</div></div>';
1059
+ // \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
1060
+ // PIPELINE
1061
+ // \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
1062
+ function tcard(t){
1063
+ return '<div class="tcard"><div class="tcard-trace">'+t.trace_id+'</div><div>'+esc(t.todo_description)+'</div><div class="tcard-meta">'+t.assignee_id+' | '+new Date(t.deadline).toLocaleString()+'</div></div>';
694
1064
  }
695
1065
  async function loadPipeline(el){
696
- el.innerHTML='<div class="empty">Loading...</div>';
1066
+ el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u4E2D...</p></div>';
697
1067
  try{
1068
+ try{const ar=await fetch(API+'/nodes/status');const ad=await ar.json();cachedAgents=ad.agents||[]}catch{}
698
1069
  const r=await fetch(API+'/jobs/active');
699
1070
  const d=await r.json();
700
- if(!d.jobs.length){el.innerHTML='<div class="empty">No active jobs. Create one via the API or CLI.</div>';return}
701
- let h='';
1071
+ let h='<div class="section-hd"><h2>\u5F02\u6B65\u7F16\u6392\u5927\u76D8</h2><button class="btn btn-primary btn-sm" onclick="showCreateJob()">+ \u521B\u5EFA\u4EFB\u52A1</button></div>';
1072
+ if(!d.jobs.length){
1073
+ h+='<div class="empty-state" style="margin-top:32px"><div class="icon">\u{1F4CB}</div><p>\u6682\u65E0\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\u3002\u70B9\u51FB\u4E0A\u65B9\u300C+ \u521B\u5EFA\u4EFB\u52A1\u300D\uFF0C\u8F93\u5165\u9700\u6C42\u540E AI \u81EA\u52A8\u89C4\u5212\u5206\u53D1\u3002';
1074
+ if(!cachedAgents.length)h+='<br/><br/>\u26A0\uFE0F \u9700\u8981\u5148\u5728\u300C\u78B3\u57FA\u7B97\u529B\u6C60\u300D\u4E2D\u6DFB\u52A0\u7269\u7406\u8282\u70B9\u3002';
1075
+ h+='</p></div>';
1076
+ el.innerHTML=h;return;
1077
+ }
702
1078
  for(const j of d.jobs){
703
1079
  const res=j.tasks.filter(t=>t.status==='RESOLVED').length;
704
1080
  const tot=j.tasks.length;
@@ -706,55 +1082,234 @@ async function loadPipeline(el){
706
1082
  const dispatched=j.tasks.filter(t=>t.status==='DISPATCHED'||t.status==='PENDING');
707
1083
  const overdue=j.tasks.filter(t=>t.status==='OVERDUE');
708
1084
  const done=j.tasks.filter(t=>t.status==='RESOLVED');
709
- h+='<div class="job-card"><div class="job-header"><span class="job-title">'+esc(j.original_prompt)+'</span><span class="job-id">'+j.job_id+'</span></div>';
710
- h+='<div class="progress-label">'+res+'/'+tot+' completed ('+pct+'%)</div>';
711
- h+='<div class="progress-bar"><div class="progress-fill" style="width:'+pct+'%"></div></div>';
712
- h+='<div class="kanban"><div class="kanban-lane"><div class="lane-title dispatched">Dispatched ('+dispatched.length+')</div>'+dispatched.map(taskCard).join('')+'</div>';
713
- h+='<div class="kanban-lane"><div class="lane-title overdue">Overdue ('+overdue.length+')</div>'+overdue.map(taskCard).join('')+'</div>';
714
- h+='<div class="kanban-lane"><div class="lane-title resolved">Resolved ('+done.length+')</div>'+done.map(taskCard).join('')+'</div></div></div>';
1085
+ 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>';
1086
+ 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>';
1087
+ if(pct===100)h+='<div style="margin-bottom:12px"><button class="btn btn-green btn-sm" onclick="syncJob(\\''+j.job_id+'\\')">\u805A\u5408\u5E76\u540C\u6B65\u5230 OpenClaw</button></div>';
1088
+ h+='<div class="kanban"><div class="lane"><div class="lane-hd y">\u5DF2\u5206\u53D1 ('+dispatched.length+')</div>'+dispatched.map(tcard).join('')+'</div>';
1089
+ h+='<div class="lane"><div class="lane-hd r">\u5DF2\u8D85\u65F6 ('+overdue.length+')</div>'+overdue.map(tcard).join('')+'</div>';
1090
+ h+='<div class="lane"><div class="lane-hd g">\u5DF2\u4EA4\u4ED8 ('+done.length+')</div>'+done.map(tcard).join('')+'</div></div></div>';
715
1091
  }
716
1092
  el.innerHTML=h;
717
- }catch{el.innerHTML='<div class="empty">Failed to load pipeline data</div>'}
1093
+ }catch(e){el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u5931\u8D25: '+e.message+'</p></div>'}
718
1094
  }
719
1095
 
720
- // \u2500\u2500\u2500 Terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
721
- function showToast(msg,type){
722
- const old=document.querySelector('.toast');if(old)old.remove();
723
- const t=document.createElement('div');t.className='toast '+type;t.textContent=msg;document.body.appendChild(t);
724
- setTimeout(()=>t.remove(),3000);
1096
+ // \u2500\u2500\u2500 AI Planning Flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1097
+ window.showCreateJob=function(){
1098
+ if(!cachedAgents.length){toast('\u8BF7\u5148\u5728\u300C\u78B3\u57FA\u7B97\u529B\u6C60\u300D\u4E2D\u6DFB\u52A0\u81F3\u5C11\u4E00\u4E2A\u7269\u7406\u8282\u70B9',false);return}
1099
+ currentPlan=null;
1100
+ // Pre-select all IDLE agents
1101
+ selectedAgentIds=new Set(cachedAgents.filter(a=>a.status==='IDLE').map(a=>a.agent_id));
1102
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
1103
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
1104
+ renderPlanStep1(ov);
1105
+ document.body.appendChild(ov);
1106
+ };
1107
+
1108
+ function renderPlanStep1(ov){
1109
+ let chipFilter='all';
1110
+ function renderChips(filter){
1111
+ chipFilter=filter;
1112
+ const filtered=filter==='idle'?cachedAgents.filter(a=>a.status==='IDLE'):cachedAgents;
1113
+ let ch='<div class="chip-bar">';
1114
+ ch+='<span class="chip-filter'+(filter==='all'?' active':'')+'" onclick="window._filterChips(\\'all\\')">\u5168\u90E8</span>';
1115
+ ch+='<span class="chip-filter'+(filter==='idle'?' active':'')+'" onclick="window._filterChips(\\'idle\\')">\u4EC5\u7A7A\u95F2</span>';
1116
+ ch+='</div>';
1117
+ ch+='<div class="agent-chips">';
1118
+ for(const a of filtered){
1119
+ const sel=selectedAgentIds.has(a.agent_id);
1120
+ ch+='<div class="achip'+(sel?' selected':'')+'" onclick="window._toggleChip(\\''+a.agent_id+'\\')">';
1121
+ ch+='<span class="adot '+a.status+'"></span>';
1122
+ ch+=esc(a.name);
1123
+ ch+='</div>';
1124
+ }
1125
+ ch+='</div>';
1126
+ return ch;
1127
+ }
1128
+ window._filterChips=function(f){
1129
+ chipFilter=f;
1130
+ const el=document.getElementById('agent-chip-area');
1131
+ if(el)el.innerHTML=renderChips(f);
1132
+ };
1133
+ window._toggleChip=function(id){
1134
+ if(selectedAgentIds.has(id))selectedAgentIds.delete(id);
1135
+ else selectedAgentIds.add(id);
1136
+ const el=document.getElementById('agent-chip-area');
1137
+ if(el)el.innerHTML=renderChips(chipFilter);
1138
+ };
1139
+
1140
+ ov.innerHTML='<div class="form-card"><h3>AI \u667A\u80FD\u89C4\u5212</h3>'
1141
+ +'<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"></textarea></div>'
1142
+ +'<div class="fg"><label>\u9009\u62E9\u53C2\u4E0E\u7684\u7269\u7406\u8282\u70B9 <span style="color:var(--text-dim);font-weight:400">(\u9ED8\u8BA4\u9009\u4E2D\u7A7A\u95F2\u8282\u70B9)</span></label>'
1143
+ +'<div id="agent-chip-area">'+renderChips('all')+'</div></div>'
1144
+ +'<div class="fg"><label>OpenClaw \u56DE\u8C03\u5730\u5740 <span style="color:var(--text-dim);font-weight:400">(\u53EF\u9009)</span></label><input id="plan-callback" placeholder="https://..."/></div>'
1145
+ +'<div class="btn-group">'
1146
+ +'<button class="btn btn-primary" onclick="planWithAI()">AI \u89C4\u5212</button>'
1147
+ +'<button class="btn btn-ghost" onclick="showManualCreate()">\u624B\u52A8\u521B\u5EFA</button>'
1148
+ +'<button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button>'
1149
+ +'</div></div>';
1150
+ setTimeout(()=>{const ta=document.getElementById('plan-prompt');if(ta)ta.focus()},50);
1151
+ };
1152
+
1153
+ window.planWithAI=async function(){
1154
+ const prompt=document.getElementById('plan-prompt').value.trim();
1155
+ const callback=document.getElementById('plan-callback')?.value.trim()||'';
1156
+ if(!prompt){toast('\u8BF7\u8F93\u5165\u9700\u6C42\u63CF\u8FF0',false);return}
1157
+ if(selectedAgentIds.size===0){toast('\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u7269\u7406\u8282\u70B9',false);return}
1158
+
1159
+ const ov=document.getElementById('overlay');
1160
+ const fc=ov.querySelector('.form-card');
1161
+ 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>';
1162
+
1163
+ try{
1164
+ const r=await fetch(API+'/jobs/plan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent_ids:Array.from(selectedAgentIds)})});
1165
+ const d=await r.json();
1166
+ if(!r.ok){
1167
+ toast(d.error||'\u89C4\u5212\u5931\u8D25',false);
1168
+ renderPlanStep1(ov);
1169
+ return;
1170
+ }
1171
+ currentPlan={...d,openclaw_callback:callback};
1172
+ renderPlanStep2(ov);
1173
+ }catch(e){
1174
+ toast('\u7F51\u7EDC\u9519\u8BEF: '+e.message,false);
1175
+ renderPlanStep1(ov);
1176
+ }
1177
+ };
1178
+
1179
+ function renderPlanStep2(ov){
1180
+ const p=currentPlan;
1181
+ let h='<h3>\u89C4\u5212\u9884\u89C8</h3>';
1182
+ h+='<div style="font-size:12px;color:var(--text-dim);margin-bottom:14px">\u9700\u6C42: '+esc(p.original_prompt)+'</div>';
1183
+ for(let i=0;i<p.planned_tasks.length;i++){
1184
+ const t=p.planned_tasks[i];
1185
+ const agent=cachedAgents.find(a=>a.agent_id===t.assignee_id);
1186
+ const status=agent?agent.status:'IDLE';
1187
+ h+='<div class="plan-card">';
1188
+ h+='<div class="plan-card-hd"><span class="adot '+status+'"></span><span class="plan-card-agent">'+esc(t.assignee_name)+'</span><span class="plan-card-agentid">'+t.assignee_id+'</span></div>';
1189
+ h+='<div class="plan-card-desc">'+esc(t.todo_description)+'</div>';
1190
+ h+='<div class="briefing-box"><div class="briefing-label">\u8BDD\u672F (\u53EF\u76F4\u63A5\u53D1\u7ED9 TA)</div><button class="briefing-copy" onclick="window._copyBriefing('+i+')">\u590D\u5236</button>'+esc(t.briefing)+'</div>';
1191
+ h+='<div class="plan-card-dl">\u622A\u6B62: '+new Date(t.deadline).toLocaleString()+'</div>';
1192
+ h+='</div>';
1193
+ }
1194
+ h+='<div class="btn-group">';
1195
+ h+='<button class="btn btn-green" onclick="dispatchPlan()">\u786E\u8BA4\u5206\u53D1</button>';
1196
+ h+='<button class="btn btn-ghost" onclick="renderPlanStep1(document.getElementById(\\'overlay\\'))">\u91CD\u65B0\u89C4\u5212</button>';
1197
+ h+='<button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button>';
1198
+ h+='</div>';
1199
+ ov.querySelector('.form-card').innerHTML=h;
725
1200
  }
1201
+ window.renderPlanStep1=renderPlanStep1;
1202
+
1203
+ window._copyBriefing=function(i){
1204
+ const text=currentPlan.planned_tasks[i].briefing;
1205
+ navigator.clipboard.writeText(text).then(()=>toast('\u8BDD\u672F\u5DF2\u590D\u5236',true)).catch(()=>toast('\u590D\u5236\u5931\u8D25',false));
1206
+ };
1207
+
1208
+ window.dispatchPlan=async function(){
1209
+ const p=currentPlan;
1210
+ const tasks=p.planned_tasks.map(t=>({assignee_id:t.assignee_id,todo_description:t.todo_description,deadline:t.deadline}));
1211
+ try{
1212
+ const r=await fetch(API+'/jobs/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({original_prompt:p.original_prompt,openclaw_callback:p.openclaw_callback||'',tasks})});
1213
+ const d=await r.json();
1214
+ if(!r.ok){toast(d.error||'\u5206\u53D1\u5931\u8D25',false);return}
1215
+ toast('\u4EFB\u52A1\u5DF2\u5206\u53D1\uFF01Job: '+d.job_id,true);
1216
+ document.getElementById('overlay').remove();
1217
+ load('pipeline');
1218
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1219
+ };
1220
+
1221
+ // \u2500\u2500\u2500 Manual creation (fallback) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1222
+ window.showManualCreate=function(){
1223
+ const ov=document.getElementById('overlay');
1224
+ const optionsHtml=cachedAgents.map(a=>'<option value="'+a.agent_id+'">'+esc(a.name)+' ('+a.agent_id+')</option>').join('');
1225
+ const tomorrow=new Date(Date.now()+86400000).toISOString().slice(0,16);
1226
+ ov.querySelector('.form-card').innerHTML='<h3>\u624B\u52A8\u521B\u5EFA\u4EFB\u52A1</h3>'
1227
+ +'<div class="fg"><label>\u4EFB\u52A1\u63CF\u8FF0 (\u539F\u59CB\u9700\u6C42)</label><input id="cj-prompt" placeholder="\u4F8B: \u5B8C\u6210\u9996\u9875\u6539\u7248"/></div>'
1228
+ +'<div class="fg"><label>OpenClaw \u56DE\u8C03\u5730\u5740 (\u53EF\u9009)</label><input id="cj-callback" placeholder="https://..."/></div>'
1229
+ +'<div style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center"><label style="font-size:13px;font-weight:600;color:var(--text)">\u5B50\u4EFB\u52A1\u5217\u8868</label><button class="btn btn-ghost btn-sm" onclick="addTaskRow()">+ \u6DFB\u52A0\u5B50\u4EFB\u52A1</button></div>'
1230
+ +'<div id="task-rows"><div class="task-row"><div class="fg"><label>\u6307\u6D3E\u8282\u70B9</label><select class="tr-agent">'+optionsHtml+'</select></div><div class="fg"><label>\u4EFB\u52A1\u63CF\u8FF0</label><input class="tr-desc" placeholder="\u5177\u4F53\u8981\u505A\u4EC0\u4E48..."/></div><div class="fg"><label>\u622A\u6B62\u65F6\u95F4</label><input class="tr-deadline" type="datetime-local" value="'+tomorrow+'"/></div></div></div>'
1231
+ +'<div class="btn-group"><button class="btn btn-primary" onclick="submitJob()">\u521B\u5EFA\u5E76\u5206\u53D1</button><button class="btn btn-ghost" onclick="renderPlanStep1(document.getElementById(\\'overlay\\'))">\u8FD4\u56DE AI \u89C4\u5212</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div>';
1232
+ };
1233
+ window.addTaskRow=function(){
1234
+ const optionsHtml=cachedAgents.map(a=>'<option value="'+a.agent_id+'">'+esc(a.name)+' ('+a.agent_id+')</option>').join('');
1235
+ const tomorrow=new Date(Date.now()+86400000).toISOString().slice(0,16);
1236
+ const row=document.createElement('div');row.className='task-row';
1237
+ row.innerHTML='<button class="remove-task" onclick="this.parentElement.remove()">&times;</button>'
1238
+ +'<div class="fg"><label>\u6307\u6D3E\u8282\u70B9</label><select class="tr-agent">'+optionsHtml+'</select></div>'
1239
+ +'<div class="fg"><label>\u4EFB\u52A1\u63CF\u8FF0</label><input class="tr-desc" placeholder="\u5177\u4F53\u8981\u505A\u4EC0\u4E48..."/></div>'
1240
+ +'<div class="fg"><label>\u622A\u6B62\u65F6\u95F4</label><input class="tr-deadline" type="datetime-local" value="'+tomorrow+'"/></div>';
1241
+ document.getElementById('task-rows').appendChild(row);
1242
+ };
1243
+ window.submitJob=async function(){
1244
+ const prompt=document.getElementById('cj-prompt').value.trim();
1245
+ const callback=document.getElementById('cj-callback').value.trim();
1246
+ if(!prompt){toast('\u8BF7\u8F93\u5165\u4EFB\u52A1\u63CF\u8FF0',false);return}
1247
+ const rows=document.querySelectorAll('.task-row');
1248
+ const tasks=[];
1249
+ for(const row of rows){
1250
+ const aid=row.querySelector('.tr-agent').value;
1251
+ const desc=row.querySelector('.tr-desc').value.trim();
1252
+ const dl=row.querySelector('.tr-deadline').value;
1253
+ if(!desc){toast('\u6BCF\u4E2A\u5B50\u4EFB\u52A1\u90FD\u9700\u8981\u586B\u5199\u63CF\u8FF0',false);return}
1254
+ tasks.push({assignee_id:aid,todo_description:desc,deadline:new Date(dl).toISOString()});
1255
+ }
1256
+ if(!tasks.length){toast('\u81F3\u5C11\u6DFB\u52A0\u4E00\u4E2A\u5B50\u4EFB\u52A1',false);return}
1257
+ try{
1258
+ const r=await fetch(API+'/jobs/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({original_prompt:prompt,openclaw_callback:callback,tasks})});
1259
+ const d=await r.json();
1260
+ if(!r.ok){toast(d.error||'\u521B\u5EFA\u5931\u8D25',false);return}
1261
+ toast('\u4EFB\u52A1\u5DF2\u521B\u5EFA\u5E76\u5206\u53D1\uFF01Job: '+d.job_id,true);
1262
+ document.getElementById('overlay').remove();
1263
+ load('pipeline');
1264
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1265
+ };
1266
+ window.syncJob=async function(jobId){
1267
+ try{
1268
+ const r=await fetch(API+'/jobs/'+jobId+'/sync',{method:'POST'});
1269
+ const d=await r.json();
1270
+ if(!r.ok){toast(d.error||'\u540C\u6B65\u5931\u8D25',false);return}
1271
+ toast('\u805A\u5408\u5B8C\u6210\uFF01'+d.sync.message,true);
1272
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1273
+ };
1274
+
1275
+ // \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
1276
+ // TERMINAL
1277
+ // \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
726
1278
  function loadTerminal(el){
727
- el.innerHTML='<div class="terminal-section"><h2>&gt; I/O Resolution Terminal</h2>'
728
- +'<div class="form-group"><label for="trace-id">Trace ID</label><input type="text" id="trace-id" placeholder="e.g. TK-9527"/></div>'
729
- +'<div class="form-group"><label for="result-text">Delivery Payload (text)</label><textarea id="result-text" placeholder="Paste the worker\\'s deliverable, summary, or code here..."></textarea></div>'
730
- +'<div class="btn-group"><button class="btn btn-primary" id="btn-submit">Submit &amp; Resume</button><button class="btn btn-danger" id="btn-reject">Reject &amp; Retry</button></div></div>';
731
- document.getElementById('btn-submit').addEventListener('click',async()=>{
732
- const tid=document.getElementById('trace-id').value.trim();
733
- const txt=document.getElementById('result-text').value.trim();
734
- if(!tid){showToast('Trace ID is required','error');return}
735
- if(!txt){showToast('Delivery payload is required','error');return}
736
- try{
737
- const r=await fetch(API+'/tasks/resume',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({trace_id:tid,result_data:{text:txt}})});
738
- const d=await r.json();
739
- if(!r.ok){showToast(d.error||'Failed','error');return}
740
- showToast(d.job_complete?'Task resolved! Job complete.':'Task '+tid+' resolved.','success');
741
- document.getElementById('trace-id').value='';document.getElementById('result-text').value='';
742
- }catch{showToast('Network error','error')}
743
- });
744
- document.getElementById('btn-reject').addEventListener('click',async()=>{
745
- const tid=document.getElementById('trace-id').value.trim();
746
- if(!tid){showToast('Trace ID is required','error');return}
747
- try{
748
- const r=await fetch(API+'/tasks/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({trace_id:tid})});
749
- const d=await r.json();
750
- if(!r.ok){showToast(d.error||'Failed','error');return}
751
- showToast('Task '+tid+' rejected and deadline extended.','success');
752
- document.getElementById('trace-id').value='';document.getElementById('result-text').value='';
753
- }catch{showToast('Network error','error')}
754
- });
1279
+ el.innerHTML='<div class="section-hd"><h2>I/O \u4EA4\u4ED8\u7EC8\u7AEF</h2></div>'
1280
+ +'<div class="form-card" style="margin-top:12px"><h3>> \u63D0\u4EA4\u7269\u7406\u8282\u70B9\u4EA7\u51FA</h3>'
1281
+ +'<div class="fg"><label>Trace ID (\u8FFD\u8E2A\u7801)</label><input id="t-tid" placeholder="\u4F8B: TK-9527"/></div>'
1282
+ +'<div class="fg"><label>\u4EA4\u4ED8\u8F7D\u8377</label><textarea id="t-payload" placeholder="\u7C98\u8D34\u4EA4\u4ED8\u7269\u5185\u5BB9\u3001\u5DE5\u4F5C\u6C47\u62A5\u6216\u4EE3\u7801..."></textarea></div>'
1283
+ +'<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>';
755
1284
  }
1285
+ window.doResume=async function(){
1286
+ const tid=document.getElementById('t-tid').value.trim();
1287
+ const payload=document.getElementById('t-payload').value.trim();
1288
+ if(!tid){toast('\u8BF7\u8F93\u5165 Trace ID',false);return}
1289
+ if(!payload){toast('\u8BF7\u8F93\u5165\u4EA4\u4ED8\u8F7D\u8377',false);return}
1290
+ try{
1291
+ const r=await fetch(API+'/tasks/resume',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({trace_id:tid,result_data:{text:payload}})});
1292
+ const d=await r.json();
1293
+ if(!r.ok){toast(d.error||'\u63D0\u4EA4\u5931\u8D25',false);return}
1294
+ toast(d.job_complete?'\u4EFB\u52A1\u5DF2\u4EA4\u4ED8\uFF01Job \u5DF2\u5168\u90E8\u5B8C\u6210\uFF0C\u53EF\u4EE5\u805A\u5408\u540C\u6B65\u3002':'\u4EFB\u52A1 '+tid+' \u5DF2\u4EA4\u4ED8\u3002',true);
1295
+ document.getElementById('t-tid').value='';document.getElementById('t-payload').value='';
1296
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1297
+ };
1298
+ window.doReject=async function(){
1299
+ const tid=document.getElementById('t-tid').value.trim();
1300
+ if(!tid){toast('\u8BF7\u8F93\u5165 Trace ID',false);return}
1301
+ try{
1302
+ const r=await fetch(API+'/tasks/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({trace_id:tid})});
1303
+ const d=await r.json();
1304
+ if(!r.ok){toast(d.error||'\u64CD\u4F5C\u5931\u8D25',false);return}
1305
+ toast('\u4EFB\u52A1 '+tid+' \u5DF2\u6253\u56DE\uFF0C\u622A\u6B62\u65F6\u95F4\u5DF2\u5EF6\u957F 24 \u5C0F\u65F6\u3002',true);
1306
+ document.getElementById('t-tid').value='';document.getElementById('t-payload').value='';
1307
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1308
+ };
756
1309
 
757
- // \u2500\u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1310
+ // \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
1311
+ // TABS & INIT
1312
+ // \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
758
1313
  const tabs=document.querySelectorAll('.tab');
759
1314
  const panels=document.querySelectorAll('.panel');
760
1315
  function load(id){
@@ -775,7 +1330,7 @@ load('fleet');
775
1330
  setInterval(()=>{
776
1331
  const a=document.querySelector('.tab.active');
777
1332
  if(a&&a.dataset.panel!=='terminal')load(a.dataset.panel);
778
- },10000);
1333
+ },15000);
779
1334
  </script>
780
1335
  </body>
781
1336
  </html>`;
@@ -911,5 +1466,78 @@ program.command("status").description("Show active jobs overview").action(() =>
911
1466
  console.log();
912
1467
  }
913
1468
  });
1469
+ program.command("plan").description("AI-powered task planning from natural language").argument("[prompt]", "What you want to get done").option("--agents <ids>", "Comma-separated agent IDs (default: all IDLE)").option("--dispatch", "Automatically dispatch after planning").action(async (promptArg, opts) => {
1470
+ const db2 = getDb();
1471
+ initSchema(db2);
1472
+ p.intro(chalk.bgCyan(" AI \u667A\u80FD\u89C4\u5212 "));
1473
+ let prompt = promptArg;
1474
+ if (!prompt) {
1475
+ const input = await p.text({
1476
+ message: "\u8F93\u5165\u4F60\u7684\u9700\u6C42:",
1477
+ placeholder: "\u4F8B: \u5B8C\u6210\u9996\u9875\u91CD\u6784\uFF0C\u5305\u62EC\u5BFC\u822A\u680F\u548C\u5185\u5BB9\u533A\u7684\u54CD\u5E94\u5F0F\u6539\u7248",
1478
+ validate: (v) => !v ? "\u9700\u6C42\u63CF\u8FF0\u4E0D\u80FD\u4E3A\u7A7A" : void 0
1479
+ });
1480
+ if (p.isCancel(input)) {
1481
+ p.cancel("\u5DF2\u53D6\u6D88");
1482
+ process.exit(0);
1483
+ }
1484
+ prompt = input;
1485
+ }
1486
+ const agentIds = opts?.agents?.split(",").map((s) => s.trim()).filter(Boolean);
1487
+ const spin = p.spinner();
1488
+ spin.start("AI \u6B63\u5728\u5206\u6790\u9700\u6C42\u3001\u5339\u914D\u8282\u70B9\u3001\u751F\u6210\u8BDD\u672F...");
1489
+ try {
1490
+ const plan = await planJob({ prompt, agent_ids: agentIds }, void 0, db2);
1491
+ spin.stop("\u89C4\u5212\u5B8C\u6210\uFF01");
1492
+ console.log(chalk.bold(`
1493
+ \u9700\u6C42: ${chalk.white(plan.original_prompt)}
1494
+ `));
1495
+ for (const task of plan.planned_tasks) {
1496
+ console.log(chalk.cyan(` \u250C\u2500 ${chalk.bold(task.assignee_name)} (${chalk.dim(task.assignee_id)})`));
1497
+ console.log(chalk.cyan(" \u2502") + ` \u4EFB\u52A1: ${task.todo_description}`);
1498
+ console.log(chalk.cyan(" \u2502") + ` \u8BDD\u672F: ${chalk.italic(task.briefing)}`);
1499
+ console.log(chalk.cyan(" \u2502") + ` \u622A\u6B62: ${new Date(task.deadline).toLocaleString()}`);
1500
+ console.log(chalk.cyan(" \u2514\u2500"));
1501
+ console.log();
1502
+ }
1503
+ if (opts?.dispatch) {
1504
+ const tasks = plan.planned_tasks.map((t) => ({
1505
+ assignee_id: t.assignee_id,
1506
+ todo_description: t.todo_description,
1507
+ deadline: t.deadline
1508
+ }));
1509
+ const job = dispatchJob({
1510
+ original_prompt: plan.original_prompt,
1511
+ openclaw_callback: "",
1512
+ tasks
1513
+ }, db2);
1514
+ p.outro(`${chalk.green("\u5DF2\u5206\u53D1\uFF01")} Job: ${chalk.bold(job.job_id)}`);
1515
+ } else {
1516
+ const confirm2 = await p.confirm({
1517
+ message: "\u786E\u8BA4\u5206\u53D1\u8FD9\u4E9B\u4EFB\u52A1\uFF1F"
1518
+ });
1519
+ if (p.isCancel(confirm2) || !confirm2) {
1520
+ p.outro(chalk.dim("\u5DF2\u53D6\u6D88\u5206\u53D1"));
1521
+ process.exit(0);
1522
+ }
1523
+ const tasks = plan.planned_tasks.map((t) => ({
1524
+ assignee_id: t.assignee_id,
1525
+ todo_description: t.todo_description,
1526
+ deadline: t.deadline
1527
+ }));
1528
+ const job = dispatchJob({
1529
+ original_prompt: plan.original_prompt,
1530
+ openclaw_callback: "",
1531
+ tasks
1532
+ }, db2);
1533
+ p.outro(`${chalk.green("\u5DF2\u5206\u53D1\uFF01")} Job: ${chalk.bold(job.job_id)}`);
1534
+ }
1535
+ } catch (error) {
1536
+ spin.stop("\u89C4\u5212\u5931\u8D25");
1537
+ const message = error instanceof Error ? error.message : "Unknown error";
1538
+ p.outro(chalk.red(message));
1539
+ process.exit(1);
1540
+ }
1541
+ });
914
1542
  program.parse();
915
1543
  //# sourceMappingURL=index.js.map