@humanclaw/humanclaw 1.0.2 → 1.1.1

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
@@ -67,6 +67,11 @@ function initSchema(db2) {
67
67
  CREATE INDEX IF NOT EXISTS idx_tasks_job_id ON tasks(job_id);
68
68
  CREATE INDEX IF NOT EXISTS idx_tasks_assignee_id ON tasks(assignee_id);
69
69
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
70
+
71
+ CREATE TABLE IF NOT EXISTS config (
72
+ key TEXT PRIMARY KEY,
73
+ value TEXT NOT NULL
74
+ );
70
75
  `);
71
76
  }
72
77
 
@@ -368,6 +373,264 @@ function dispatchJob(request, db2) {
368
373
  return { ...job, tasks };
369
374
  }
370
375
 
376
+ // src/llm/claude.ts
377
+ var ClaudeProvider = class {
378
+ apiKey;
379
+ model;
380
+ constructor(apiKey, model) {
381
+ this.apiKey = apiKey;
382
+ this.model = model || "claude-sonnet-4-20250514";
383
+ }
384
+ async complete(request) {
385
+ const systemMessages = request.messages.filter((m) => m.role === "system");
386
+ const nonSystemMessages = request.messages.filter((m) => m.role !== "system");
387
+ const body = {
388
+ model: this.model,
389
+ max_tokens: request.max_tokens || 4096,
390
+ messages: nonSystemMessages.map((m) => ({ role: m.role, content: m.content }))
391
+ };
392
+ if (request.temperature !== void 0) {
393
+ body.temperature = request.temperature;
394
+ }
395
+ if (systemMessages.length > 0) {
396
+ body.system = systemMessages.map((m) => m.content).join("\n\n");
397
+ }
398
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
399
+ method: "POST",
400
+ headers: {
401
+ "Content-Type": "application/json",
402
+ "x-api-key": this.apiKey,
403
+ "anthropic-version": "2023-06-01"
404
+ },
405
+ body: JSON.stringify(body)
406
+ });
407
+ if (!response.ok) {
408
+ const errorText = await response.text();
409
+ throw new Error(`Claude API error (${response.status}): ${errorText}`);
410
+ }
411
+ const data = await response.json();
412
+ const textBlock = data.content.find((c) => c.type === "text");
413
+ if (!textBlock) {
414
+ throw new Error("Claude API returned no text content");
415
+ }
416
+ return { content: textBlock.text };
417
+ }
418
+ };
419
+
420
+ // src/llm/openai.ts
421
+ var OpenAIProvider = class {
422
+ apiKey;
423
+ model;
424
+ constructor(apiKey, model) {
425
+ this.apiKey = apiKey;
426
+ this.model = model || "gpt-4o";
427
+ }
428
+ async complete(request) {
429
+ const body = {
430
+ model: this.model,
431
+ max_tokens: request.max_tokens || 4096,
432
+ messages: request.messages.map((m) => ({ role: m.role, content: m.content }))
433
+ };
434
+ if (request.temperature !== void 0) {
435
+ body.temperature = request.temperature;
436
+ }
437
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
438
+ method: "POST",
439
+ headers: {
440
+ "Content-Type": "application/json",
441
+ "Authorization": `Bearer ${this.apiKey}`
442
+ },
443
+ body: JSON.stringify(body)
444
+ });
445
+ if (!response.ok) {
446
+ const errorText = await response.text();
447
+ throw new Error(`OpenAI API error (${response.status}): ${errorText}`);
448
+ }
449
+ const data = await response.json();
450
+ const content = data.choices[0]?.message?.content;
451
+ if (!content) {
452
+ throw new Error("OpenAI API returned no content");
453
+ }
454
+ return { content };
455
+ }
456
+ };
457
+
458
+ // src/models/config.ts
459
+ function getConfig(key, db2) {
460
+ const conn = db2 ?? getDb();
461
+ const row = conn.prepare("SELECT value FROM config WHERE key = ?").get(key);
462
+ return row?.value;
463
+ }
464
+ function setConfig(key, value, db2) {
465
+ const conn = db2 ?? getDb();
466
+ conn.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)").run(key, value);
467
+ }
468
+ function deleteConfig(key, db2) {
469
+ const conn = db2 ?? getDb();
470
+ conn.prepare("DELETE FROM config WHERE key = ?").run(key);
471
+ }
472
+
473
+ // src/llm/index.ts
474
+ function getLlmConfig() {
475
+ const dbProvider = getConfig("llm_provider");
476
+ const dbApiKey = getConfig("llm_api_key");
477
+ const dbModel = getConfig("llm_model");
478
+ const provider = dbProvider || process.env.HUMANCLAW_LLM_PROVIDER || "claude";
479
+ const apiKey = dbApiKey || process.env.HUMANCLAW_LLM_API_KEY || "";
480
+ const model = dbModel || process.env.HUMANCLAW_LLM_MODEL;
481
+ if (!apiKey) {
482
+ throw new Error(
483
+ "\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"
484
+ );
485
+ }
486
+ if (provider !== "claude" && provider !== "openai") {
487
+ throw new Error(`\u4E0D\u652F\u6301\u7684 LLM \u63D0\u4F9B\u5546: ${provider}\u3002\u652F\u6301: claude, openai`);
488
+ }
489
+ return { provider, apiKey, model: model || void 0 };
490
+ }
491
+ function createLlmProvider(config) {
492
+ const cfg = config || getLlmConfig();
493
+ switch (cfg.provider) {
494
+ case "claude":
495
+ return new ClaudeProvider(cfg.apiKey, cfg.model);
496
+ case "openai":
497
+ return new OpenAIProvider(cfg.apiKey, cfg.model);
498
+ }
499
+ }
500
+
501
+ // src/services/planner.ts
502
+ function buildSystemPrompt() {
503
+ 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
504
+
505
+ \u89C4\u5219\uFF1A
506
+ 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
507
+ 2. \u6839\u636E\u6BCF\u4E2A Agent \u7684\u6280\u80FD\uFF08capabilities\uFF09\u548C\u5F53\u524D\u8D1F\u8F7D\u6765\u5339\u914D\u5206\u914D
508
+ 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
509
+ 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
510
+ 5. \u6BCF\u4E2A Agent \u6700\u591A\u5206\u914D\u4E00\u4E2A\u4EFB\u52A1\uFF08\u9664\u975E\u4EBA\u624B\u4E0D\u591F\uFF09
511
+
512
+ \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
513
+
514
+ \`\`\`json
515
+ [
516
+ {
517
+ "assignee_id": "agent\u7684ID",
518
+ "assignee_name": "agent\u7684\u540D\u5B57",
519
+ "todo_description": "\u7B80\u77ED\u7684\u4EFB\u52A1\u6807\u9898/\u63CF\u8FF0",
520
+ "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",
521
+ "deadline": "ISO 8601 \u65F6\u95F4"
522
+ }
523
+ ]
524
+ \`\`\``;
525
+ }
526
+ function buildUserPrompt(prompt, agents) {
527
+ const now = /* @__PURE__ */ new Date();
528
+ const agentList = agents.map((a) => {
529
+ 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";
530
+ const speed = a.avg_delivery_hours !== null ? `\u5E73\u5747\u4EA4\u4ED8\u65F6\u95F4 ${a.avg_delivery_hours}h` : "\u6682\u65E0\u5386\u53F2\u6570\u636E";
531
+ return `- ${a.name} (ID: ${a.agent_id}) \u6280\u80FD: [${a.capabilities.join(", ")}] ${load} ${speed}`;
532
+ }).join("\n");
533
+ return `\u5F53\u524D\u65F6\u95F4: ${now.toISOString()}
534
+
535
+ \u53EF\u7528\u7684\u7269\u7406\u8282\u70B9\uFF1A
536
+ ${agentList}
537
+
538
+ \u9700\u6C42\uFF1A
539
+ ${prompt}
540
+
541
+ \u8BF7\u6839\u636E\u4EE5\u4E0A\u4FE1\u606F\u62C6\u89E3\u4EFB\u52A1\u5E76\u5206\u914D\u3002\u8F93\u51FA JSON \u6570\u7EC4\u3002`;
542
+ }
543
+ function extractJson(raw) {
544
+ const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
545
+ if (codeBlockMatch) {
546
+ return codeBlockMatch[1].trim();
547
+ }
548
+ const arrayMatch = raw.match(/\[[\s\S]*\]/);
549
+ if (arrayMatch) {
550
+ return arrayMatch[0];
551
+ }
552
+ return raw.trim();
553
+ }
554
+ function validatePlannedTasks(parsed, validAgentIds, agents) {
555
+ if (!Array.isArray(parsed)) {
556
+ throw new Error("LLM \u8FD4\u56DE\u7684\u4E0D\u662F\u6709\u6548\u7684\u4EFB\u52A1\u6570\u7EC4");
557
+ }
558
+ if (parsed.length === 0) {
559
+ throw new Error("LLM \u672A\u751F\u6210\u4EFB\u4F55\u4EFB\u52A1");
560
+ }
561
+ return parsed.map((item, i) => {
562
+ if (!item.todo_description || typeof item.todo_description !== "string") {
563
+ throw new Error(`\u4EFB\u52A1 ${i + 1} \u7F3A\u5C11 todo_description`);
564
+ }
565
+ if (!item.briefing || typeof item.briefing !== "string") {
566
+ throw new Error(`\u4EFB\u52A1 ${i + 1} \u7F3A\u5C11 briefing (\u8BDD\u672F)`);
567
+ }
568
+ let assigneeId = String(item.assignee_id || "");
569
+ let assigneeName = String(item.assignee_name || "");
570
+ if (!validAgentIds.has(assigneeId)) {
571
+ const fallback = agents[0];
572
+ if (fallback) {
573
+ assigneeId = fallback.agent_id;
574
+ assigneeName = fallback.name;
575
+ }
576
+ }
577
+ let deadline = String(item.deadline || "");
578
+ if (!deadline || isNaN(Date.parse(deadline))) {
579
+ deadline = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
580
+ }
581
+ return {
582
+ assignee_id: assigneeId,
583
+ assignee_name: assigneeName,
584
+ todo_description: String(item.todo_description),
585
+ briefing: String(item.briefing),
586
+ deadline
587
+ };
588
+ });
589
+ }
590
+ async function planJob(request, provider, db2) {
591
+ const conn = db2 ?? getDb();
592
+ const allAgents = listAgentsWithMetrics(conn);
593
+ let agents;
594
+ if (request.agent_ids && request.agent_ids.length > 0) {
595
+ agents = allAgents.filter((a) => request.agent_ids.includes(a.agent_id));
596
+ if (agents.length === 0) {
597
+ throw new Error("\u6307\u5B9A\u7684 Agent \u5747\u4E0D\u5B58\u5728");
598
+ }
599
+ } else {
600
+ agents = allAgents.filter((a) => a.status === "IDLE");
601
+ if (agents.length === 0) {
602
+ agents = allAgents.filter((a) => a.status !== "OFFLINE" && a.status !== "OOM");
603
+ }
604
+ if (agents.length === 0) {
605
+ 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");
606
+ }
607
+ }
608
+ const llm = provider ?? createLlmProvider();
609
+ const systemPrompt = buildSystemPrompt();
610
+ const userPrompt = buildUserPrompt(request.prompt, agents);
611
+ const response = await llm.complete({
612
+ messages: [
613
+ { role: "system", content: systemPrompt },
614
+ { role: "user", content: userPrompt }
615
+ ],
616
+ temperature: 0.3,
617
+ max_tokens: 4096
618
+ });
619
+ const jsonStr = extractJson(response.content);
620
+ let parsed;
621
+ try {
622
+ parsed = JSON.parse(jsonStr);
623
+ } catch {
624
+ throw new Error("AI \u8FD4\u56DE\u7684\u5185\u5BB9\u65E0\u6CD5\u89E3\u6790\u4E3A JSON\uFF0C\u8BF7\u91CD\u8BD5");
625
+ }
626
+ const validIds = new Set(agents.map((a) => a.agent_id));
627
+ const plannedTasks = validatePlannedTasks(parsed, validIds, agents);
628
+ return {
629
+ original_prompt: request.prompt,
630
+ planned_tasks: plannedTasks
631
+ };
632
+ }
633
+
371
634
  // src/routes/jobs.ts
372
635
  var router2 = Router2();
373
636
  router2.post("/create", (req, res) => {
@@ -399,6 +662,21 @@ router2.get("/active", (_req, res) => {
399
662
  const jobs = listActiveJobs();
400
663
  res.json({ jobs });
401
664
  });
665
+ router2.post("/plan", async (req, res) => {
666
+ const body = req.body;
667
+ if (!body.prompt || typeof body.prompt !== "string") {
668
+ res.status(400).json({ error: "prompt is required" });
669
+ return;
670
+ }
671
+ try {
672
+ const plan = await planJob(body);
673
+ res.json(plan);
674
+ } catch (error) {
675
+ const message = error instanceof Error ? error.message : "Unknown error";
676
+ const status = message.includes("API Key") || message.includes("API key") ? 503 : 400;
677
+ res.status(status).json({ error: message });
678
+ }
679
+ });
402
680
  router2.get("/:job_id", (req, res) => {
403
681
  const { job_id } = req.params;
404
682
  const job = getJobWithTasks(job_id);
@@ -565,6 +843,49 @@ router4.post("/:job_id/sync", async (req, res) => {
565
843
  });
566
844
  var sync_default = router4;
567
845
 
846
+ // src/routes/config.ts
847
+ import { Router as Router5 } from "express";
848
+ var router5 = Router5();
849
+ router5.get("/", (_req, res) => {
850
+ const provider = getConfig("llm_provider") || process.env.HUMANCLAW_LLM_PROVIDER || "claude";
851
+ const apiKey = getConfig("llm_api_key") || process.env.HUMANCLAW_LLM_API_KEY || "";
852
+ const model = getConfig("llm_model") || process.env.HUMANCLAW_LLM_MODEL || "";
853
+ const keySource = getConfig("llm_api_key") ? "dashboard" : process.env.HUMANCLAW_LLM_API_KEY ? "env" : "none";
854
+ res.json({
855
+ provider,
856
+ api_key_set: apiKey.length > 0,
857
+ api_key_masked: apiKey ? apiKey.slice(0, 8) + "..." + apiKey.slice(-4) : "",
858
+ api_key_source: keySource,
859
+ model
860
+ });
861
+ });
862
+ router5.put("/", (req, res) => {
863
+ const { provider, api_key, model } = req.body;
864
+ if (provider !== void 0) {
865
+ if (provider !== "claude" && provider !== "openai") {
866
+ res.status(400).json({ error: "\u4E0D\u652F\u6301\u7684\u63D0\u4F9B\u5546\uFF0C\u652F\u6301: claude, openai" });
867
+ return;
868
+ }
869
+ setConfig("llm_provider", provider);
870
+ }
871
+ if (api_key !== void 0) {
872
+ if (api_key === "") {
873
+ deleteConfig("llm_api_key");
874
+ } else {
875
+ setConfig("llm_api_key", api_key);
876
+ }
877
+ }
878
+ if (model !== void 0) {
879
+ if (model === "") {
880
+ deleteConfig("llm_model");
881
+ } else {
882
+ setConfig("llm_model", model);
883
+ }
884
+ }
885
+ res.json({ ok: true });
886
+ });
887
+ var config_default = router5;
888
+
568
889
  // src/dashboard.ts
569
890
  function getDashboardHtml() {
570
891
  return `<!DOCTYPE html>
@@ -580,17 +901,20 @@ body{background:var(--bg);color:var(--text);font-family:var(--font-sans);min-hei
580
901
  header{padding:20px 32px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px}
581
902
  header h1{font-family:var(--font-mono);font-size:22px;color:var(--accent);letter-spacing:1.5px}
582
903
  .subtitle{color:var(--text-dim);font-size:12px;border-left:1px solid var(--border);padding-left:14px}
904
+ .hdr-spacer{flex:1}
905
+ .gear-btn{background:none;border:1px solid var(--border);color:var(--text-dim);width:34px;height:34px;border-radius:8px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;transition:all .15s}
906
+ .gear-btn:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-dim)}
583
907
  nav{display:flex;border-bottom:1px solid var(--border);padding:0 32px;gap:4px}
584
908
  .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}
585
909
  .tab:hover{color:var(--text);background:var(--surface)}
586
910
  .tab.active{color:var(--accent);border-bottom-color:var(--accent)}
587
911
  main{padding:24px 32px;max-width:1200px}
588
912
  .panel{display:none}.panel.active{display:block}
589
- /* \u2500\u2500\u2500 Cards \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 */
913
+ /* Cards */
590
914
  .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;transition:border-color .15s}
591
915
  .card:hover{border-color:color-mix(in srgb,var(--accent) 40%,transparent)}
592
916
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;margin-top:16px}
593
- /* \u2500\u2500\u2500 Agent Card \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 */
917
+ /* Agent Card */
594
918
  .agent-header{display:flex;align-items:center;gap:10px;margin-bottom:10px}
595
919
  .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
596
920
  .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)}
@@ -603,13 +927,13 @@ main{padding:24px 32px;max-width:1200px}
603
927
  .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}
604
928
  .agent-actions button{background:var(--red);color:#fff;border:none;border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer}
605
929
  .agent-actions button:hover{opacity:.8}
606
- /* \u2500\u2500\u2500 Stats Bar \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 */
930
+ /* Stats Bar */
607
931
  .stats{display:flex;gap:12px;flex-wrap:wrap}
608
932
  .stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 22px;text-align:center;min-width:90px}
609
933
  .stat-val{font-size:28px;font-weight:700;font-family:var(--font-mono)}
610
934
  .stat-lbl{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
611
- /* \u2500\u2500\u2500 Forms \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 */
612
- .form-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-top:16px;max-width:560px}
935
+ /* Forms */
936
+ .form-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-top:16px;max-width:620px}
613
937
  .form-card h3{font-size:15px;color:var(--accent);font-family:var(--font-mono);margin-bottom:16px}
614
938
  .fg{margin-bottom:14px}
615
939
  .fg label{display:block;font-size:12px;color:var(--text-dim);margin-bottom:5px;font-weight:500}
@@ -620,17 +944,18 @@ main{padding:24px 32px;max-width:1200px}
620
944
  .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}
621
945
  .btn:hover{opacity:.85;transform:translateY(-1px)}
622
946
  .btn:active{transform:translateY(0)}
947
+ .btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
623
948
  .btn-primary{background:var(--accent);color:var(--bg)}
624
949
  .btn-green{background:var(--green);color:#fff}
625
950
  .btn-danger{background:var(--red);color:#fff}
626
951
  .btn-ghost{background:transparent;color:var(--accent);border:1px solid var(--border)}
627
952
  .btn-ghost:hover{background:var(--accent-dim)}
628
953
  .btn-sm{padding:6px 14px;font-size:12px}
629
- .btn-group{display:flex;gap:10px;margin-top:16px}
630
- /* \u2500\u2500\u2500 Section header \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 */
954
+ .btn-group{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
955
+ /* Section header */
631
956
  .section-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
632
957
  .section-hd h2{font-size:16px;font-weight:600}
633
- /* \u2500\u2500\u2500 Job card \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 */
958
+ /* Job card */
634
959
  .job-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-top:14px}
635
960
  .job-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
636
961
  .job-title{font-weight:600;font-size:14px}
@@ -646,30 +971,58 @@ main{padding:24px 32px;max-width:1200px}
646
971
  .tcard{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;margin-bottom:6px;font-size:12px}
647
972
  .tcard-trace{font-family:var(--font-mono);font-size:10px;color:var(--accent);margin-bottom:3px}
648
973
  .tcard-meta{font-size:10px;color:var(--text-dim);margin-top:3px}
649
- /* \u2500\u2500\u2500 Task Row in Job Form \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
650
- .task-row{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:10px;position:relative}
651
- .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}
652
- .task-row .fg{margin-bottom:10px}
653
- .task-row .fg:last-child{margin-bottom:0}
654
- .task-row-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
655
- /* \u2500\u2500\u2500 Toast \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 */
974
+ /* Toast */
656
975
  .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)}
657
976
  .toast.ok{background:var(--green);color:#fff}.toast.err{background:var(--red);color:#fff}
658
977
  @keyframes slide-up{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
659
- /* \u2500\u2500\u2500 Empty state \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 */
978
+ /* Empty state */
660
979
  .empty-state{text-align:center;padding:48px 20px;color:var(--text-dim)}
661
980
  .empty-state .icon{font-size:48px;margin-bottom:12px;opacity:.6}
662
981
  .empty-state p{font-size:14px;margin-bottom:16px;max-width:360px;margin-left:auto;margin-right:auto;line-height:1.6}
663
- /* \u2500\u2500\u2500 Modal/Overlay \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 */
664
- .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:80px;animation:fade-in .15s ease}
665
- .overlay .form-card{max-width:520px;width:100%;max-height:80vh;overflow-y:auto;margin:0}
982
+ /* Modal/Overlay */
983
+ .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}
984
+ .overlay .form-card{max-width:620px;width:100%;max-height:85vh;overflow-y:auto;margin:0}
666
985
  @keyframes fade-in{from{opacity:0}to{opacity:1}}
986
+ /* Agent chips for AI planning */
987
+ .chip-bar{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
988
+ .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}
989
+ .chip-filter.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent)}
990
+ .agent-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
991
+ .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}
992
+ .achip:hover{border-color:var(--text-dim)}
993
+ .achip.selected{border-color:var(--accent);background:var(--accent-dim)}
994
+ .achip .adot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
995
+ .achip .adot.IDLE{background:var(--green)}.achip .adot.BUSY{background:var(--yellow)}.achip .adot.OFFLINE{background:var(--red)}.achip .adot.OOM{background:var(--purple)}
996
+ /* Plan preview cards */
997
+ .plan-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
998
+ .plan-card-hd{display:flex;align-items:center;gap:8px;margin-bottom:8px}
999
+ .plan-card-hd .adot{width:8px;height:8px;border-radius:50%}
1000
+ .plan-card-agent{font-weight:600;font-size:13px}
1001
+ .plan-card-agentid{font-family:var(--font-mono);font-size:10px;color:var(--text-dim)}
1002
+ .plan-card-desc{font-size:13px;margin-bottom:10px;font-weight:500}
1003
+ .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}
1004
+ .briefing-label{font-size:10px;color:var(--accent);font-weight:600;margin-bottom:4px;font-family:var(--font-mono)}
1005
+ .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}
1006
+ .briefing-copy:hover{color:var(--accent);border-color:var(--accent)}
1007
+ .plan-card-dl{font-size:11px;color:var(--text-dim);font-family:var(--font-mono)}
1008
+ /* Spinner */
1009
+ .spinner-wrap{text-align:center;padding:40px 20px}
1010
+ .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}
1011
+ @keyframes spin{to{transform:rotate(360deg)}}
1012
+ .spinner-text{color:var(--text-dim);font-size:13px;margin-top:12px}
1013
+ /* Manual task row */
1014
+ .task-row{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:10px;position:relative}
1015
+ .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}
1016
+ .task-row .fg{margin-bottom:10px}
1017
+ .task-row .fg:last-child{margin-bottom:0}
667
1018
  </style>
668
1019
  </head>
669
1020
  <body>
670
1021
  <header>
671
1022
  <h1>HumanClaw</h1>
672
1023
  <span class="subtitle">\u5F02\u6B65\u7269\u7406\u8282\u70B9\u7F16\u6392\u6846\u67B6</span>
1024
+ <span class="hdr-spacer"></span>
1025
+ <button class="gear-btn" onclick="showSettings()" title="\u8BBE\u7F6E">&#9881;</button>
673
1026
  </header>
674
1027
  <nav>
675
1028
  <button class="tab active" data-panel="fleet">\u78B3\u57FA\u7B97\u529B\u6C60</button>
@@ -691,6 +1044,8 @@ function toast(msg,ok){
691
1044
  setTimeout(()=>t.remove(),3500);
692
1045
  }
693
1046
  let cachedAgents=[];
1047
+ let currentPlan=null;
1048
+ let selectedAgentIds=new Set();
694
1049
 
695
1050
  // \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
696
1051
  // FLEET
@@ -781,13 +1136,12 @@ function tcard(t){
781
1136
  async function loadPipeline(el){
782
1137
  el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u4E2D...</p></div>';
783
1138
  try{
784
- // refresh agents cache
785
1139
  try{const ar=await fetch(API+'/nodes/status');const ad=await ar.json();cachedAgents=ad.agents||[]}catch{}
786
1140
  const r=await fetch(API+'/jobs/active');
787
1141
  const d=await r.json();
788
1142
  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>';
789
1143
  if(!d.jobs.length){
790
- 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\u5F00\u59CB\u4F60\u7684\u7B2C\u4E00\u6B21\u7269\u7406\u7F16\u6392\u3002';
1144
+ 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';
791
1145
  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';
792
1146
  h+='</p></div>';
793
1147
  el.innerHTML=h;return;
@@ -809,19 +1163,143 @@ async function loadPipeline(el){
809
1163
  el.innerHTML=h;
810
1164
  }catch(e){el.innerHTML='<div class="empty-state"><p>\u52A0\u8F7D\u5931\u8D25: '+e.message+'</p></div>'}
811
1165
  }
1166
+
1167
+ // \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
812
1168
  window.showCreateJob=function(){
813
1169
  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}
1170
+ currentPlan=null;
1171
+ // Pre-select all IDLE agents
1172
+ selectedAgentIds=new Set(cachedAgents.filter(a=>a.status==='IDLE').map(a=>a.agent_id));
814
1173
  const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
815
1174
  ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
1175
+ renderPlanStep1(ov);
1176
+ document.body.appendChild(ov);
1177
+ };
1178
+
1179
+ function renderPlanStep1(ov){
1180
+ let chipFilter='all';
1181
+ function renderChips(filter){
1182
+ chipFilter=filter;
1183
+ const filtered=filter==='idle'?cachedAgents.filter(a=>a.status==='IDLE'):cachedAgents;
1184
+ let ch='<div class="chip-bar">';
1185
+ ch+='<span class="chip-filter'+(filter==='all'?' active':'')+'" onclick="window._filterChips(\\'all\\')">\u5168\u90E8</span>';
1186
+ ch+='<span class="chip-filter'+(filter==='idle'?' active':'')+'" onclick="window._filterChips(\\'idle\\')">\u4EC5\u7A7A\u95F2</span>';
1187
+ ch+='</div>';
1188
+ ch+='<div class="agent-chips">';
1189
+ for(const a of filtered){
1190
+ const sel=selectedAgentIds.has(a.agent_id);
1191
+ ch+='<div class="achip'+(sel?' selected':'')+'" onclick="window._toggleChip(\\''+a.agent_id+'\\')">';
1192
+ ch+='<span class="adot '+a.status+'"></span>';
1193
+ ch+=esc(a.name);
1194
+ ch+='</div>';
1195
+ }
1196
+ ch+='</div>';
1197
+ return ch;
1198
+ }
1199
+ window._filterChips=function(f){
1200
+ chipFilter=f;
1201
+ const el=document.getElementById('agent-chip-area');
1202
+ if(el)el.innerHTML=renderChips(f);
1203
+ };
1204
+ window._toggleChip=function(id){
1205
+ if(selectedAgentIds.has(id))selectedAgentIds.delete(id);
1206
+ else selectedAgentIds.add(id);
1207
+ const el=document.getElementById('agent-chip-area');
1208
+ if(el)el.innerHTML=renderChips(chipFilter);
1209
+ };
1210
+
1211
+ ov.innerHTML='<div class="form-card"><h3>AI \u667A\u80FD\u89C4\u5212</h3>'
1212
+ +'<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>'
1213
+ +'<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>'
1214
+ +'<div id="agent-chip-area">'+renderChips('all')+'</div></div>'
1215
+ +'<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>'
1216
+ +'<div class="btn-group">'
1217
+ +'<button class="btn btn-primary" onclick="planWithAI()">AI \u89C4\u5212</button>'
1218
+ +'<button class="btn btn-ghost" onclick="showManualCreate()">\u624B\u52A8\u521B\u5EFA</button>'
1219
+ +'<button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button>'
1220
+ +'</div></div>';
1221
+ setTimeout(()=>{const ta=document.getElementById('plan-prompt');if(ta)ta.focus()},50);
1222
+ };
1223
+
1224
+ window.planWithAI=async function(){
1225
+ const prompt=document.getElementById('plan-prompt').value.trim();
1226
+ const callback=document.getElementById('plan-callback')?.value.trim()||'';
1227
+ if(!prompt){toast('\u8BF7\u8F93\u5165\u9700\u6C42\u63CF\u8FF0',false);return}
1228
+ if(selectedAgentIds.size===0){toast('\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u7269\u7406\u8282\u70B9',false);return}
1229
+
1230
+ const ov=document.getElementById('overlay');
1231
+ const fc=ov.querySelector('.form-card');
1232
+ 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>';
1233
+
1234
+ try{
1235
+ const r=await fetch(API+'/jobs/plan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent_ids:Array.from(selectedAgentIds)})});
1236
+ const d=await r.json();
1237
+ if(!r.ok){
1238
+ toast(d.error||'\u89C4\u5212\u5931\u8D25',false);
1239
+ renderPlanStep1(ov);
1240
+ return;
1241
+ }
1242
+ currentPlan={...d,openclaw_callback:callback};
1243
+ renderPlanStep2(ov);
1244
+ }catch(e){
1245
+ toast('\u7F51\u7EDC\u9519\u8BEF: '+e.message,false);
1246
+ renderPlanStep1(ov);
1247
+ }
1248
+ };
1249
+
1250
+ function renderPlanStep2(ov){
1251
+ const p=currentPlan;
1252
+ let h='<h3>\u89C4\u5212\u9884\u89C8</h3>';
1253
+ h+='<div style="font-size:12px;color:var(--text-dim);margin-bottom:14px">\u9700\u6C42: '+esc(p.original_prompt)+'</div>';
1254
+ for(let i=0;i<p.planned_tasks.length;i++){
1255
+ const t=p.planned_tasks[i];
1256
+ const agent=cachedAgents.find(a=>a.agent_id===t.assignee_id);
1257
+ const status=agent?agent.status:'IDLE';
1258
+ h+='<div class="plan-card">';
1259
+ 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>';
1260
+ h+='<div class="plan-card-desc">'+esc(t.todo_description)+'</div>';
1261
+ 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>';
1262
+ h+='<div class="plan-card-dl">\u622A\u6B62: '+new Date(t.deadline).toLocaleString()+'</div>';
1263
+ h+='</div>';
1264
+ }
1265
+ h+='<div class="btn-group">';
1266
+ h+='<button class="btn btn-green" onclick="dispatchPlan()">\u786E\u8BA4\u5206\u53D1</button>';
1267
+ h+='<button class="btn btn-ghost" onclick="renderPlanStep1(document.getElementById(\\'overlay\\'))">\u91CD\u65B0\u89C4\u5212</button>';
1268
+ h+='<button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button>';
1269
+ h+='</div>';
1270
+ ov.querySelector('.form-card').innerHTML=h;
1271
+ }
1272
+ window.renderPlanStep1=renderPlanStep1;
1273
+
1274
+ window._copyBriefing=function(i){
1275
+ const text=currentPlan.planned_tasks[i].briefing;
1276
+ navigator.clipboard.writeText(text).then(()=>toast('\u8BDD\u672F\u5DF2\u590D\u5236',true)).catch(()=>toast('\u590D\u5236\u5931\u8D25',false));
1277
+ };
1278
+
1279
+ window.dispatchPlan=async function(){
1280
+ const p=currentPlan;
1281
+ const tasks=p.planned_tasks.map(t=>({assignee_id:t.assignee_id,todo_description:t.todo_description,deadline:t.deadline}));
1282
+ try{
1283
+ 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})});
1284
+ const d=await r.json();
1285
+ if(!r.ok){toast(d.error||'\u5206\u53D1\u5931\u8D25',false);return}
1286
+ toast('\u4EFB\u52A1\u5DF2\u5206\u53D1\uFF01Job: '+d.job_id,true);
1287
+ document.getElementById('overlay').remove();
1288
+ load('pipeline');
1289
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1290
+ };
1291
+
1292
+ // \u2500\u2500\u2500 Manual creation (fallback) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1293
+ window.showManualCreate=function(){
1294
+ const ov=document.getElementById('overlay');
816
1295
  const optionsHtml=cachedAgents.map(a=>'<option value="'+a.agent_id+'">'+esc(a.name)+' ('+a.agent_id+')</option>').join('');
817
1296
  const tomorrow=new Date(Date.now()+86400000).toISOString().slice(0,16);
818
- ov.innerHTML='<div class="form-card"><h3>+ \u521B\u5EFA\u7F16\u6392\u4EFB\u52A1</h3>'
1297
+ ov.querySelector('.form-card').innerHTML='<h3>\u624B\u52A8\u521B\u5EFA\u4EFB\u52A1</h3>'
819
1298
  +'<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>'
820
1299
  +'<div class="fg"><label>OpenClaw \u56DE\u8C03\u5730\u5740 (\u53EF\u9009)</label><input id="cj-callback" placeholder="https://..."/></div>'
821
1300
  +'<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>'
822
- +'<div id="task-rows"><div class="task-row" data-idx="0"><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>'
823
- +'<div class="btn-group"><button class="btn btn-primary" onclick="submitJob()">\u521B\u5EFA\u5E76\u5206\u53D1</button><button class="btn btn-ghost" onclick="document.getElementById(\\'overlay\\').remove()">\u53D6\u6D88</button></div></div>';
824
- document.body.appendChild(ov);
1301
+ +'<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>'
1302
+ +'<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>';
825
1303
  };
826
1304
  window.addTaskRow=function(){
827
1305
  const optionsHtml=cachedAgents.map(a=>'<option value="'+a.agent_id+'">'+esc(a.name)+' ('+a.agent_id+')</option>').join('');
@@ -900,6 +1378,50 @@ window.doReject=async function(){
900
1378
  }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
901
1379
  };
902
1380
 
1381
+ // \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
1382
+ // SETTINGS
1383
+ // \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
1384
+ window.showSettings=async function(){
1385
+ const ov=document.createElement('div');ov.className='overlay';ov.id='overlay';
1386
+ ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()});
1387
+ ov.innerHTML='<div class="form-card"><h3>LLM \u8BBE\u7F6E</h3><div class="spinner-wrap"><div class="spinner"></div></div></div>';
1388
+ document.body.appendChild(ov);
1389
+
1390
+ try{
1391
+ const r=await fetch(API+'/config');
1392
+ const cfg=await r.json();
1393
+ const fc=ov.querySelector('.form-card');
1394
+ let statusHtml='';
1395
+ if(cfg.api_key_set){
1396
+ const src=cfg.api_key_source==='dashboard'?'Dashboard \u914D\u7F6E':'\u73AF\u5883\u53D8\u91CF';
1397
+ statusHtml='<div style="background:var(--accent-dim);border:1px solid color-mix(in srgb,var(--accent) 25%,transparent);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:12px"><span style="color:var(--green)">&#10003;</span> API Key \u5DF2\u914D\u7F6E <span style="color:var(--text-dim)">('+esc(cfg.api_key_masked)+' | \u6765\u6E90: '+src+')</span></div>';
1398
+ }else{
1399
+ 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>';
1400
+ }
1401
+ fc.innerHTML='<h3>LLM \u8BBE\u7F6E</h3>'+statusHtml
1402
+ +'<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>'
1403
+ +'<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>'
1404
+ +'<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>'
1405
+ +'<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>';
1406
+ }catch{
1407
+ ov.querySelector('.form-card').innerHTML='<h3>LLM \u8BBE\u7F6E</h3><p style="color:var(--red)">\u52A0\u8F7D\u914D\u7F6E\u5931\u8D25</p>';
1408
+ }
1409
+ };
1410
+ window.saveSettings=async function(){
1411
+ const provider=document.getElementById('cfg-provider').value;
1412
+ const apiKey=document.getElementById('cfg-key').value.trim();
1413
+ const model=document.getElementById('cfg-model').value.trim();
1414
+ const body={provider};
1415
+ if(apiKey)body.api_key=apiKey;
1416
+ body.model=model;
1417
+ try{
1418
+ const r=await fetch(API+'/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
1419
+ if(!r.ok){const d=await r.json();toast(d.error||'\u4FDD\u5B58\u5931\u8D25',false);return}
1420
+ toast('\u8BBE\u7F6E\u5DF2\u4FDD\u5B58',true);
1421
+ document.getElementById('overlay').remove();
1422
+ }catch{toast('\u7F51\u7EDC\u9519\u8BEF',false)}
1423
+ };
1424
+
903
1425
  // \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
904
1426
  // TABS & INIT
905
1427
  // \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
@@ -940,6 +1462,7 @@ function createServer(port = 2026) {
940
1462
  app.use("/api/v1/jobs", jobs_default);
941
1463
  app.use("/api/v1/tasks", tasks_default);
942
1464
  app.use("/api/v1/jobs", sync_default);
1465
+ app.use("/api/v1/config", config_default);
943
1466
  const dashboardHtml = getDashboardHtml();
944
1467
  app.get("/", (_req, res) => {
945
1468
  res.type("html").send(dashboardHtml);
@@ -1059,5 +1582,78 @@ program.command("status").description("Show active jobs overview").action(() =>
1059
1582
  console.log();
1060
1583
  }
1061
1584
  });
1585
+ 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) => {
1586
+ const db2 = getDb();
1587
+ initSchema(db2);
1588
+ p.intro(chalk.bgCyan(" AI \u667A\u80FD\u89C4\u5212 "));
1589
+ let prompt = promptArg;
1590
+ if (!prompt) {
1591
+ const input = await p.text({
1592
+ message: "\u8F93\u5165\u4F60\u7684\u9700\u6C42:",
1593
+ placeholder: "\u4F8B: \u5B8C\u6210\u9996\u9875\u91CD\u6784\uFF0C\u5305\u62EC\u5BFC\u822A\u680F\u548C\u5185\u5BB9\u533A\u7684\u54CD\u5E94\u5F0F\u6539\u7248",
1594
+ validate: (v) => !v ? "\u9700\u6C42\u63CF\u8FF0\u4E0D\u80FD\u4E3A\u7A7A" : void 0
1595
+ });
1596
+ if (p.isCancel(input)) {
1597
+ p.cancel("\u5DF2\u53D6\u6D88");
1598
+ process.exit(0);
1599
+ }
1600
+ prompt = input;
1601
+ }
1602
+ const agentIds = opts?.agents?.split(",").map((s) => s.trim()).filter(Boolean);
1603
+ const spin = p.spinner();
1604
+ spin.start("AI \u6B63\u5728\u5206\u6790\u9700\u6C42\u3001\u5339\u914D\u8282\u70B9\u3001\u751F\u6210\u8BDD\u672F...");
1605
+ try {
1606
+ const plan = await planJob({ prompt, agent_ids: agentIds }, void 0, db2);
1607
+ spin.stop("\u89C4\u5212\u5B8C\u6210\uFF01");
1608
+ console.log(chalk.bold(`
1609
+ \u9700\u6C42: ${chalk.white(plan.original_prompt)}
1610
+ `));
1611
+ for (const task of plan.planned_tasks) {
1612
+ console.log(chalk.cyan(` \u250C\u2500 ${chalk.bold(task.assignee_name)} (${chalk.dim(task.assignee_id)})`));
1613
+ console.log(chalk.cyan(" \u2502") + ` \u4EFB\u52A1: ${task.todo_description}`);
1614
+ console.log(chalk.cyan(" \u2502") + ` \u8BDD\u672F: ${chalk.italic(task.briefing)}`);
1615
+ console.log(chalk.cyan(" \u2502") + ` \u622A\u6B62: ${new Date(task.deadline).toLocaleString()}`);
1616
+ console.log(chalk.cyan(" \u2514\u2500"));
1617
+ console.log();
1618
+ }
1619
+ if (opts?.dispatch) {
1620
+ const tasks = plan.planned_tasks.map((t) => ({
1621
+ assignee_id: t.assignee_id,
1622
+ todo_description: t.todo_description,
1623
+ deadline: t.deadline
1624
+ }));
1625
+ const job = dispatchJob({
1626
+ original_prompt: plan.original_prompt,
1627
+ openclaw_callback: "",
1628
+ tasks
1629
+ }, db2);
1630
+ p.outro(`${chalk.green("\u5DF2\u5206\u53D1\uFF01")} Job: ${chalk.bold(job.job_id)}`);
1631
+ } else {
1632
+ const confirm2 = await p.confirm({
1633
+ message: "\u786E\u8BA4\u5206\u53D1\u8FD9\u4E9B\u4EFB\u52A1\uFF1F"
1634
+ });
1635
+ if (p.isCancel(confirm2) || !confirm2) {
1636
+ p.outro(chalk.dim("\u5DF2\u53D6\u6D88\u5206\u53D1"));
1637
+ process.exit(0);
1638
+ }
1639
+ const tasks = plan.planned_tasks.map((t) => ({
1640
+ assignee_id: t.assignee_id,
1641
+ todo_description: t.todo_description,
1642
+ deadline: t.deadline
1643
+ }));
1644
+ const job = dispatchJob({
1645
+ original_prompt: plan.original_prompt,
1646
+ openclaw_callback: "",
1647
+ tasks
1648
+ }, db2);
1649
+ p.outro(`${chalk.green("\u5DF2\u5206\u53D1\uFF01")} Job: ${chalk.bold(job.job_id)}`);
1650
+ }
1651
+ } catch (error) {
1652
+ spin.stop("\u89C4\u5212\u5931\u8D25");
1653
+ const message = error instanceof Error ? error.message : "Unknown error";
1654
+ p.outro(chalk.red(message));
1655
+ process.exit(1);
1656
+ }
1657
+ });
1062
1658
  program.parse();
1063
1659
  //# sourceMappingURL=index.js.map