@qlingzzy/qling 1.0.0 → 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.
Files changed (254) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.en.md +83 -18
  3. package/README.md +19 -9
  4. package/dist/agent/main-loop.d.ts +120 -0
  5. package/dist/agent/main-loop.d.ts.map +1 -0
  6. package/dist/agent/main-loop.js +279 -0
  7. package/dist/agent/subtask-parallel.d.ts +29 -0
  8. package/dist/agent/subtask-parallel.d.ts.map +1 -0
  9. package/dist/agent/subtask-parallel.js +107 -0
  10. package/dist/agent/subtask.d.ts +13 -0
  11. package/dist/agent/subtask.d.ts.map +1 -1
  12. package/dist/agent/subtask.js +79 -24
  13. package/dist/agent/system-prompt.d.ts +39 -0
  14. package/dist/agent/system-prompt.d.ts.map +1 -0
  15. package/dist/agent/system-prompt.js +89 -0
  16. package/dist/agent/tool-orchestrator.d.ts +66 -0
  17. package/dist/agent/tool-orchestrator.d.ts.map +1 -0
  18. package/dist/agent/tool-orchestrator.js +355 -0
  19. package/dist/agent-loop.d.ts +29 -16
  20. package/dist/agent-loop.d.ts.map +1 -1
  21. package/dist/agent-loop.js +262 -807
  22. package/dist/agents/roles.d.ts +57 -0
  23. package/dist/agents/roles.d.ts.map +1 -0
  24. package/dist/agents/roles.js +198 -0
  25. package/dist/commands/agents.d.ts.map +1 -1
  26. package/dist/commands/agents.js +17 -3
  27. package/dist/commands/claude-style.d.ts.map +1 -1
  28. package/dist/commands/claude-style.js +19 -10
  29. package/dist/commands/help.d.ts.map +1 -1
  30. package/dist/commands/help.js +3 -2
  31. package/dist/commands/index.d.ts.map +1 -1
  32. package/dist/commands/index.js +6 -0
  33. package/dist/commands/mode.d.ts +16 -0
  34. package/dist/commands/mode.d.ts.map +1 -0
  35. package/dist/commands/mode.js +67 -0
  36. package/dist/commands/recover.d.ts +3 -0
  37. package/dist/commands/recover.d.ts.map +1 -0
  38. package/dist/commands/recover.js +69 -0
  39. package/dist/commands/runtime.d.ts +2 -0
  40. package/dist/commands/runtime.d.ts.map +1 -1
  41. package/dist/commands/runtime.js +2 -0
  42. package/dist/commands/trace.d.ts +3 -0
  43. package/dist/commands/trace.d.ts.map +1 -0
  44. package/dist/commands/trace.js +45 -0
  45. package/dist/commands/verify.js +2 -2
  46. package/dist/config-report.d.ts +0 -1
  47. package/dist/config-report.d.ts.map +1 -1
  48. package/dist/config-report.js +1 -2
  49. package/dist/config.d.ts +0 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -1
  52. package/dist/context-report.d.ts +13 -7
  53. package/dist/context-report.d.ts.map +1 -1
  54. package/dist/context-report.js +64 -61
  55. package/dist/context-tool-hygiene.d.ts +36 -0
  56. package/dist/context-tool-hygiene.d.ts.map +1 -0
  57. package/dist/context-tool-hygiene.js +125 -0
  58. package/dist/dashboard/client.d.ts +2 -0
  59. package/dist/dashboard/client.d.ts.map +1 -0
  60. package/dist/dashboard/client.js +305 -0
  61. package/dist/dashboard/model.d.ts +13 -0
  62. package/dist/dashboard/model.d.ts.map +1 -0
  63. package/dist/dashboard/model.js +112 -0
  64. package/dist/dashboard/page.d.ts +3 -0
  65. package/dist/dashboard/page.d.ts.map +1 -0
  66. package/dist/dashboard/page.js +204 -0
  67. package/dist/dashboard/types.d.ts +63 -0
  68. package/dist/dashboard/types.d.ts.map +1 -0
  69. package/dist/dashboard/types.js +1 -0
  70. package/dist/dashboard-server.d.ts +24 -4
  71. package/dist/dashboard-server.d.ts.map +1 -1
  72. package/dist/dashboard-server.js +397 -302
  73. package/dist/discovery-registry.d.ts +17 -3
  74. package/dist/discovery-registry.d.ts.map +1 -1
  75. package/dist/discovery-registry.js +78 -3
  76. package/dist/doctor.d.ts +8 -0
  77. package/dist/doctor.d.ts.map +1 -1
  78. package/dist/doctor.js +116 -0
  79. package/dist/eval/llm-tasks.d.ts +5 -0
  80. package/dist/eval/llm-tasks.d.ts.map +1 -0
  81. package/dist/eval/llm-tasks.js +125 -0
  82. package/dist/eval/repo-tasks.d.ts +12 -0
  83. package/dist/eval/repo-tasks.d.ts.map +1 -0
  84. package/dist/eval/repo-tasks.js +220 -0
  85. package/dist/eval/runner.d.ts +3 -1
  86. package/dist/eval/runner.d.ts.map +1 -1
  87. package/dist/eval/runner.js +23 -4
  88. package/dist/eval/tasks.d.ts.map +1 -1
  89. package/dist/eval/tasks.js +186 -0
  90. package/dist/eval/types.d.ts +8 -5
  91. package/dist/eval/types.d.ts.map +1 -1
  92. package/dist/execution/event-bus.d.ts +44 -0
  93. package/dist/execution/event-bus.d.ts.map +1 -0
  94. package/dist/execution/event-bus.js +76 -0
  95. package/dist/execution/failure-classifier.d.ts +11 -0
  96. package/dist/execution/failure-classifier.d.ts.map +1 -0
  97. package/dist/execution/failure-classifier.js +66 -0
  98. package/dist/execution/progress-detector.d.ts +3 -0
  99. package/dist/execution/progress-detector.d.ts.map +1 -0
  100. package/dist/execution/progress-detector.js +23 -0
  101. package/dist/execution/recovery-controller.d.ts +24 -0
  102. package/dist/execution/recovery-controller.d.ts.map +1 -0
  103. package/dist/execution/recovery-controller.js +114 -0
  104. package/dist/execution/recovery-messages.d.ts +24 -0
  105. package/dist/execution/recovery-messages.d.ts.map +1 -0
  106. package/dist/execution/recovery-messages.js +48 -0
  107. package/dist/execution/recovery-metrics.d.ts +16 -0
  108. package/dist/execution/recovery-metrics.d.ts.map +1 -0
  109. package/dist/execution/recovery-metrics.js +73 -0
  110. package/dist/execution/recovery-strategy-planner.d.ts +6 -0
  111. package/dist/execution/recovery-strategy-planner.d.ts.map +1 -0
  112. package/dist/execution/recovery-strategy-planner.js +24 -0
  113. package/dist/execution/run-trace-store.d.ts +29 -0
  114. package/dist/execution/run-trace-store.d.ts.map +1 -0
  115. package/dist/execution/run-trace-store.js +140 -0
  116. package/dist/execution/staged-verifier.d.ts +33 -0
  117. package/dist/execution/staged-verifier.d.ts.map +1 -0
  118. package/dist/execution/staged-verifier.js +39 -0
  119. package/dist/execution/types.d.ts +58 -0
  120. package/dist/execution/types.d.ts.map +1 -0
  121. package/dist/execution/types.js +1 -0
  122. package/dist/execution/verification-loop.d.ts +58 -0
  123. package/dist/execution/verification-loop.d.ts.map +1 -0
  124. package/dist/execution/verification-loop.js +195 -0
  125. package/dist/execution/verification-stages.d.ts +17 -0
  126. package/dist/execution/verification-stages.d.ts.map +1 -0
  127. package/dist/execution/verification-stages.js +102 -0
  128. package/dist/help-topics.d.ts.map +1 -1
  129. package/dist/help-topics.js +11 -0
  130. package/dist/index.js +0 -8
  131. package/dist/lsp/ts-service.d.ts +36 -0
  132. package/dist/lsp/ts-service.d.ts.map +1 -0
  133. package/dist/lsp/ts-service.js +291 -0
  134. package/dist/memory/lifecycle.d.ts +19 -0
  135. package/dist/memory/lifecycle.d.ts.map +1 -0
  136. package/dist/memory/lifecycle.js +53 -0
  137. package/dist/memory.d.ts +0 -16
  138. package/dist/memory.d.ts.map +1 -1
  139. package/dist/memory.js +0 -36
  140. package/dist/metrics/collector.d.ts +7 -0
  141. package/dist/metrics/collector.d.ts.map +1 -1
  142. package/dist/metrics/collector.js +60 -0
  143. package/dist/mission/manager.d.ts +1 -0
  144. package/dist/mission/manager.d.ts.map +1 -1
  145. package/dist/mission/manager.js +9 -0
  146. package/dist/mission/progress-notify.d.ts +39 -0
  147. package/dist/mission/progress-notify.d.ts.map +1 -0
  148. package/dist/mission/progress-notify.js +386 -0
  149. package/dist/pipeline/hooks.d.ts +1 -1
  150. package/dist/pipeline/hooks.d.ts.map +1 -1
  151. package/dist/pipeline/hooks.js +1 -0
  152. package/dist/pipeline/sections.d.ts +8 -4
  153. package/dist/pipeline/sections.d.ts.map +1 -1
  154. package/dist/pipeline/sections.js +70 -27
  155. package/dist/pipeline/verification.d.ts +1 -0
  156. package/dist/pipeline/verification.d.ts.map +1 -1
  157. package/dist/pipeline/verification.js +10 -7
  158. package/dist/providers/llm-client.d.ts +38 -0
  159. package/dist/providers/llm-client.d.ts.map +1 -0
  160. package/dist/providers/llm-client.js +105 -0
  161. package/dist/sdk.d.ts +1 -0
  162. package/dist/sdk.d.ts.map +1 -1
  163. package/dist/sdk.js +1 -0
  164. package/dist/session/durable-session-supervisor.d.ts.map +1 -1
  165. package/dist/session/durable-session-supervisor.js +13 -6
  166. package/dist/session/session-persistence.d.ts +29 -0
  167. package/dist/session/session-persistence.d.ts.map +1 -0
  168. package/dist/session/session-persistence.js +45 -0
  169. package/dist/session/session-scheduler.d.ts +8 -1
  170. package/dist/session/session-scheduler.d.ts.map +1 -1
  171. package/dist/session/session-scheduler.js +33 -4
  172. package/dist/session-task-report.d.ts +1 -0
  173. package/dist/session-task-report.d.ts.map +1 -1
  174. package/dist/session-task-report.js +2 -1
  175. package/dist/shortcuts.d.ts.map +1 -1
  176. package/dist/shortcuts.js +1 -0
  177. package/dist/skills/registry.d.ts.map +1 -1
  178. package/dist/skills/registry.js +10 -0
  179. package/dist/skills/security-scan.d.ts +15 -0
  180. package/dist/skills/security-scan.d.ts.map +1 -0
  181. package/dist/skills/security-scan.js +95 -0
  182. package/dist/skills/types.d.ts +2 -0
  183. package/dist/skills/types.d.ts.map +1 -1
  184. package/dist/statusline.d.ts +3 -3
  185. package/dist/statusline.d.ts.map +1 -1
  186. package/dist/statusline.js +9 -17
  187. package/dist/token-usage.d.ts +27 -0
  188. package/dist/token-usage.d.ts.map +1 -0
  189. package/dist/token-usage.js +107 -0
  190. package/dist/tools/bash.d.ts.map +1 -1
  191. package/dist/tools/bash.js +5 -3
  192. package/dist/tools/browser-act-session.d.ts +48 -0
  193. package/dist/tools/browser-act-session.d.ts.map +1 -0
  194. package/dist/tools/browser-act-session.js +168 -0
  195. package/dist/tools/browser-act.d.ts +14 -0
  196. package/dist/tools/browser-act.d.ts.map +1 -0
  197. package/dist/tools/browser-act.js +297 -0
  198. package/dist/tools/code-symbols.d.ts +35 -0
  199. package/dist/tools/code-symbols.d.ts.map +1 -0
  200. package/dist/tools/code-symbols.js +257 -0
  201. package/dist/tools/index.d.ts +4 -1
  202. package/dist/tools/index.d.ts.map +1 -1
  203. package/dist/tools/index.js +22 -2
  204. package/dist/tools/lsp.d.ts +13 -0
  205. package/dist/tools/lsp.d.ts.map +1 -0
  206. package/dist/tools/lsp.js +136 -0
  207. package/dist/tools/patch.d.ts +5 -0
  208. package/dist/tools/patch.d.ts.map +1 -1
  209. package/dist/tools/patch.js +25 -4
  210. package/dist/tools/search.d.ts +7 -0
  211. package/dist/tools/search.d.ts.map +1 -1
  212. package/dist/tools/search.js +20 -3
  213. package/dist/tools/skill.d.ts +2 -0
  214. package/dist/tools/skill.d.ts.map +1 -1
  215. package/dist/tools/skill.js +47 -14
  216. package/dist/tools/subtask.d.ts +3 -1
  217. package/dist/tools/subtask.d.ts.map +1 -1
  218. package/dist/tools/subtask.js +151 -56
  219. package/dist/tui/shell.d.ts +12 -0
  220. package/dist/tui/shell.d.ts.map +1 -1
  221. package/dist/tui/shell.js +36 -3
  222. package/dist/tui/streaming-repl.d.ts +1 -0
  223. package/dist/tui/streaming-repl.d.ts.map +1 -1
  224. package/dist/tui/streaming-repl.js +27 -4
  225. package/dist/tui/streaming-tui.d.ts +10 -0
  226. package/dist/tui/streaming-tui.d.ts.map +1 -1
  227. package/dist/tui/streaming-tui.js +70 -12
  228. package/dist/types.d.ts +0 -8
  229. package/dist/types.d.ts.map +1 -1
  230. package/dist/workflow-runtime.d.ts.map +1 -1
  231. package/dist/workflow-runtime.js +10 -2
  232. package/dist/workflow-types.d.ts +2 -0
  233. package/dist/workflow-types.d.ts.map +1 -1
  234. package/docs/install.md +46 -12
  235. package/docs/skills.md +56 -7
  236. package/docs/web-routing.md +83 -0
  237. package/package.json +18 -4
  238. package/scripts/dep-layers.mjs +275 -0
  239. package/scripts/eval-llm.mjs +27 -0
  240. package/scripts/eval-recovery.mjs +101 -0
  241. package/scripts/eval-tasks.mjs +20 -0
  242. package/scripts/validate-packaging.mjs +57 -0
  243. package/skills/examples/README.md +23 -0
  244. package/skills/examples/add-function/SKILL.md +31 -0
  245. package/skills/examples/fix-failing-test/SKILL.md +32 -0
  246. package/skills/examples/pr-summary/SKILL.md +34 -0
  247. package/skills/lifecycle-build/SKILL.md +22 -0
  248. package/skills/lifecycle-plan/SKILL.md +32 -0
  249. package/skills/lifecycle-review/SKILL.md +25 -0
  250. package/skills/lifecycle-ship/SKILL.md +22 -0
  251. package/skills/lifecycle-spec/SKILL.md +24 -0
  252. package/skills/lifecycle-test/SKILL.md +21 -0
  253. package/skills/opencli/SKILL.md +222 -0
  254. package/skills/qling.md +14 -3
@@ -1,325 +1,420 @@
1
- // ============================================================
2
- // 轻灵 - Observability Dashboard Server (v0.3)
3
- // 本地 Web 控制台后端,提供只读监控与读写控制接口
4
- // ============================================================
5
- import * as http from "http";
1
+ import * as http from "node:http";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { buildDashboardTasks } from "./dashboard/model.js";
5
+ import { DASHBOARD_CSS, DASHBOARD_HTML } from "./dashboard/page.js";
6
6
  import { buildLocalPermissionsReport } from "./permissions-report.js";
7
+ import { cancelLocalSessionTask, listLocalSessionTasks, } from "./session-task-report.js";
8
+ const SNAPSHOT_CACHE_MS = 750;
9
+ const DAEMON_PROBE_MS = 5_000;
10
+ const TASK_LIMIT = 50;
11
+ const TASK_SCAN_LIMIT = 5_000;
12
+ const ACTIVITY_LIMIT = 20;
13
+ const JSON_HEADERS = { "Content-Type": "application/json; charset=utf-8" };
14
+ const CLIENT_URL = new URL("./dashboard/client.js", import.meta.url);
15
+ function sendJson(res, status, value) {
16
+ res.writeHead(status, JSON_HEADERS);
17
+ res.end(JSON.stringify(value));
18
+ }
19
+ function summaryFor(tasks) {
20
+ const summary = {
21
+ total: tasks.length,
22
+ queued: 0,
23
+ running: 0,
24
+ blocked: 0,
25
+ paused: 0,
26
+ succeeded: 0,
27
+ failed: 0,
28
+ canceled: 0,
29
+ };
30
+ for (const task of tasks)
31
+ summary[task.status]++;
32
+ return summary;
33
+ }
34
+ function parseTaskRoute(pathname) {
35
+ const parts = pathname.split("/").filter(Boolean);
36
+ if (parts[0] !== "api" || parts[1] !== "tasks" || parts.length < 4 || parts.length > 5) {
37
+ return null;
38
+ }
39
+ const kind = parts[2];
40
+ if (!["mission", "loop", "workflow"].includes(kind))
41
+ return null;
42
+ try {
43
+ return {
44
+ kind,
45
+ id: decodeURIComponent(parts[3]),
46
+ action: parts[4] ? decodeURIComponent(parts[4]) : undefined,
47
+ };
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
7
53
  export class DashboardServer {
8
54
  server;
9
55
  options;
56
+ snapshotCache = null;
57
+ daemonHealthy = false;
58
+ daemonTimer = null;
59
+ clientSource = null;
10
60
  listening = false;
11
61
  constructor(options) {
12
62
  this.options = options;
13
63
  }
14
- start() {
15
- return new Promise((resolve, reject) => {
16
- this.server = http.createServer(async (req, res) => {
17
- const url = new URL(req.url || "/", `http://localhost:${this.options.port}`);
18
- // 设置 CORS
19
- res.setHeader("Access-Control-Allow-Origin", "*");
20
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
21
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
22
- if (req.method === "OPTIONS") {
23
- res.writeHead(204);
24
- res.end();
25
- return;
64
+ async start() {
65
+ if (this.listening)
66
+ return;
67
+ this.server = http.createServer((req, res) => void this.handleRequest(req, res));
68
+ await new Promise((resolve, reject) => {
69
+ const onError = (err) => {
70
+ if (err.code === "EADDRINUSE") {
71
+ reject(new Error(`EADDRINUSE: 端口 ${this.options.port} 已被占用`));
26
72
  }
27
- try {
28
- // --- 路由分发 ---
29
- // 1. 获取所有指标 (Read-only)
30
- if (url.pathname === "/api/metrics" && req.method === "GET") {
31
- const limit = Number(url.searchParams.get("limit")) || 100;
32
- const metrics = await this.options.collector.query({ limit });
33
- res.writeHead(200, { "Content-Type": "application/json" });
34
- res.end(JSON.stringify(metrics));
35
- return;
36
- }
37
- // 2. 获取当前状态 (Read-only)
38
- if (url.pathname === "/api/status" && req.method === "GET") {
39
- const checkpoint = this.options.workflowRuntime.getCheckpoint();
40
- res.writeHead(200, { "Content-Type": "application/json" });
41
- res.end(JSON.stringify({
42
- checkpoint,
43
- is_running: this.options.agentLoop.turnCount > 0,
44
- session_id: this.options.agentLoop.sessionId,
45
- }));
46
- return;
47
- }
48
- // 2b. 获取所有使命 (Read-only)
49
- if (url.pathname === "/api/missions" && req.method === "GET") {
50
- const manager = this.options.agentLoop.getMissionManager();
51
- const list = manager.listMissions();
52
- res.writeHead(200, { "Content-Type": "application/json" });
53
- res.end(JSON.stringify(list));
54
- return;
55
- }
56
- // P2 增强: sessions (只读)
57
- if (url.pathname === "/api/sessions" && req.method === "GET") {
58
- try {
59
- const limit = Number(url.searchParams.get("limit")) || 10;
60
- // 简化:从 workflow 或 agent 获取会话列表
61
- const sessions = this.options.agentLoop.getRecentSessions?.(limit)
62
- || [{ id: "current", status: "active" }];
63
- res.writeHead(200, { "Content-Type": "application/json" });
64
- res.end(JSON.stringify({ sessions }));
65
- }
66
- catch {
67
- res.writeHead(200, { "Content-Type": "application/json" });
68
- res.end(JSON.stringify({ sessions: [] }));
69
- }
70
- return;
71
- }
72
- // P2 增强: permissions (只读)
73
- if (url.pathname === "/api/permissions" && req.method === "GET") {
74
- try {
75
- const config = this.options.agentLoop.config || {};
76
- const report = buildLocalPermissionsReport({
77
- defaultMode: config.guard?.permissions?.default || "ask",
78
- rules: config.guard?.permissions?.rules || [],
79
- env: process.env,
80
- });
81
- res.writeHead(200, { "Content-Type": "application/json" });
82
- res.end(JSON.stringify(report));
83
- }
84
- catch {
85
- res.writeHead(200, { "Content-Type": "application/json" });
86
- res.end(JSON.stringify({ defaultMode: "ask", rules: [] }));
87
- }
88
- return;
89
- }
90
- // P2 增强: doctor 简要快照 (只读)
91
- if (url.pathname === "/api/doctor" && req.method === "GET") {
92
- res.writeHead(200, { "Content-Type": "application/json" });
93
- res.end(JSON.stringify({
94
- status: "ok",
95
- node: process.version,
96
- dashboard: true,
97
- features: { dashboard: true },
98
- note: "使用 /doctor 命令获取完整本地诊断"
99
- }));
100
- return;
101
- }
102
- // 3. 执行控制 (Read-write)
103
- if (url.pathname === "/api/control/pause" && req.method === "POST") {
104
- // 物理实现:通过 agentLoop 发射暂停信号 (M3 后续完善信号量)
105
- this.options.agentLoop.emit("control_signal", "pause");
106
- res.writeHead(200);
107
- res.end(JSON.stringify({ ok: true, action: "pause", status: "pausing" }));
108
- return;
109
- }
110
- if (url.pathname === "/api/control/resume" && req.method === "POST") {
111
- this.options.agentLoop.emit("control_signal", "resume");
112
- res.writeHead(200);
113
- res.end(JSON.stringify({ ok: true, action: "resume", status: "resuming" }));
114
- return;
115
- }
116
- // 4. 静态资源 (前端) - P2: 本地只读可观测控制台 (纯 HTML,无外部依赖)
117
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
118
- res.end(`<!DOCTYPE html>
119
- <html lang="zh-CN">
120
- <head>
121
- <meta charset="UTF-8">
122
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
123
- <title>轻灵 Dashboard · 本地可观测</title>
124
- <style>
125
- :root { --bg:#0f172a; --card:#1e2937; --text:#e2e8f0; --accent:#22d3ee; --green:#4ade80; --red:#f87171; }
126
- body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--bg); color: var(--text); margin:0; padding:20px; }
127
- .header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
128
- .card { background:var(--card); border-radius:8px; padding:16px; margin-bottom:16px; border:1px solid #334155; }
129
- .grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(260px,1fr)); gap:16px; }
130
- .section-title { font-size:14px; color:#94a3b8; margin-bottom:8px; text-transform:uppercase; letter-spacing:0.5px; }
131
- .metric { font-size:13px; line-height:1.6; }
132
- .metric b { color:var(--accent); }
133
- table { width:100%; border-collapse:collapse; font-size:12px; }
134
- th, td { padding:6px 8px; text-align:left; border-bottom:1px solid #334155; }
135
- th { color:#64748b; }
136
- .pill { display:inline-block; padding:1px 8px; border-radius:999px; font-size:11px; background:#334155; }
137
- .ok { color:var(--green); } .warn { color:#facc15; } .err { color:var(--red); }
138
- .log { background:#0b1120; padding:8px; border-radius:4px; font-size:11px; white-space:pre-wrap; max-height:160px; overflow:auto; }
139
- button { background:var(--accent); color:#0f172a; border:none; padding:6px 12px; border-radius:4px; cursor:pointer; font-size:12px; }
140
- button:disabled { opacity:0.5; cursor:not-allowed; }
141
- .small { font-size:11px; color:#64748b; }
142
- </style>
143
- </head>
144
- <body>
145
- <div class="header">
146
- <div>
147
- <h1 style="margin:0;font-size:20px;">轻灵 · 本地 Dashboard</h1>
148
- <div class="small">端口 <span id="port"></span> · 只读观测 · <span id="status">加载中...</span></div>
149
- </div>
150
- <div>
151
- <button onclick="refreshAll()">刷新</button>
152
- <button onclick="pauseMission()" style="margin-left:8px;">暂停</button>
153
- <button onclick="resumeMission()" style="margin-left:4px;">恢复</button>
154
- </div>
155
- </div>
156
-
157
- <div class="grid">
158
- <div class="card">
159
- <div class="section-title">当前状态</div>
160
- <div id="status-panel" class="metric">加载中...</div>
161
- </div>
162
-
163
- <div class="card">
164
- <div class="section-title">权限与边界</div>
165
- <div id="perm-panel" class="metric">加载中...</div>
166
- </div>
167
-
168
- <div class="card">
169
- <div class="section-title">Missions / 任务</div>
170
- <div id="missions-panel">加载中...</div>
171
- </div>
172
-
173
- <div class="card">
174
- <div class="section-title">会话 Sessions</div>
175
- <div id="sessions-panel">加载中...</div>
176
- </div>
177
-
178
- <div class="card">
179
- <div class="section-title">Doctor 快照</div>
180
- <div id="doctor-panel">加载中...</div>
181
- </div>
182
-
183
- <div class="card">
184
- <div class="section-title">最近指标 / 工具调用 (最近 20)</div>
185
- <div id="metrics-panel" class="log">加载中...</div>
186
- </div>
187
- </div>
188
-
189
- <div class="card">
190
- <div class="section-title">API 端点(只读优先)</div>
191
- <div class="small">
192
- GET /api/status &nbsp;|&nbsp; GET /api/missions &nbsp;|&nbsp; GET /api/metrics?limit=50<br>
193
- POST /api/control/pause &nbsp;|&nbsp; POST /api/control/resume
194
- </div>
195
- </div>
196
-
197
- <script>
198
- const port = window.location.port || '9999';
199
- document.getElementById('port').textContent = port;
200
-
201
- async function fetchJSON(path) {
202
- const res = await fetch('/' + path);
203
- if (!res.ok) throw new Error(res.status);
204
- return res.json();
205
- }
206
-
207
- async function loadStatus() {
208
- try {
209
- const s = await fetchJSON('api/status');
210
- const el = document.getElementById('status-panel');
211
- el.innerHTML = \`
212
- <b>运行中</b>: \${s.is_running ? '是' : '否'}<br>
213
- <b>会话</b>: \${s.session_id || '-'}<br>
214
- <b>Checkpoint</b>: \${s.checkpoint ? '有' : '无'}
215
- \`;
216
- document.getElementById('status').textContent = '就绪';
217
- document.getElementById('status').className = 'ok';
218
- } catch(e) {
219
- document.getElementById('status-panel').textContent = '无法获取状态';
220
- }
73
+ else
74
+ reject(err);
75
+ };
76
+ this.server.once("error", onError);
77
+ this.server.listen(this.options.port, "127.0.0.1", () => {
78
+ this.server.off("error", onError);
79
+ this.listening = true;
80
+ console.error(`🚀 Dashboard 运行在: http://127.0.0.1:${this.options.port}`);
81
+ resolve();
82
+ });
83
+ });
84
+ void this.probeDaemon();
85
+ this.daemonTimer = setInterval(() => void this.probeDaemon(), DAEMON_PROBE_MS);
86
+ this.daemonTimer.unref?.();
221
87
  }
222
-
223
- async function loadMissions() {
224
- try {
225
- const list = await fetchJSON('api/missions');
226
- const el = document.getElementById('missions-panel');
227
- if (!list || list.length === 0) { el.innerHTML = '<div class="small">暂无活跃使命</div>'; return; }
228
- el.innerHTML = list.slice(0,5).map(m => \`
229
- <div class="metric">
230
- <b>\${m.id?.slice(0,12) || 'mission'}</b> · \${m.status || ''}<br>
231
- <span class="small">\${(m.goal || '').slice(0,60)}</span>
232
- </div>
233
- \`).join('');
234
- } catch(e) { document.getElementById('missions-panel').innerHTML = '获取失败'; }
88
+ stop() {
89
+ if (this.daemonTimer)
90
+ clearInterval(this.daemonTimer);
91
+ this.daemonTimer = null;
92
+ this.server?.close();
93
+ this.listening = false;
235
94
  }
236
-
237
- async function loadMetrics() {
238
- try {
239
- const data = await fetchJSON('api/metrics?limit=20');
240
- const el = document.getElementById('metrics-panel');
241
- if (!data || !data.length) { el.textContent = '暂无指标'; return; }
242
- el.textContent = data.slice(0,12).map(m => {
243
- const t = new Date(m.ts || Date.now()).toLocaleTimeString();
244
- return \`\${t} | \${m.type || 'event'} \${JSON.stringify(m.payload || {}).slice(0,80)}\`;
245
- }).join('\\n');
246
- } catch(e) { document.getElementById('metrics-panel').textContent = '获取失败'; }
95
+ setSecurityHeaders(res) {
96
+ res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
97
+ res.setHeader("X-Content-Type-Options", "nosniff");
98
+ res.setHeader("X-Frame-Options", "DENY");
99
+ res.setHeader("Referrer-Policy", "no-referrer");
247
100
  }
248
-
249
- async function loadPerm() {
250
- try {
251
- const p = await fetchJSON('api/permissions');
252
- const el = document.getElementById('perm-panel');
253
- el.innerHTML = \`
254
- <b>默认</b>: \${p.defaultMode || 'ask'}<br>
255
- <b>规则</b>: \${p.rules?.length || 0}<br>
256
- <span class="small">本地权限边界</span>
257
- \`;
258
- } catch(e) {
259
- const el = document.getElementById('perm-panel');
260
- el.innerHTML = '权限信息获取失败';
261
- }
101
+ sameOriginAllowed(req) {
102
+ const origin = req.headers.origin;
103
+ if (!origin)
104
+ return true;
105
+ return origin === `http://127.0.0.1:${this.options.port}` || origin === `http://localhost:${this.options.port}`;
262
106
  }
263
-
264
- async function loadSessions() {
265
- try {
266
- const s = await fetchJSON('api/sessions');
267
- let el = document.getElementById('sessions-panel');
268
- if (!el) return;
269
- const list = s.sessions || [];
270
- el.innerHTML = list.length ? list.slice(0,3).map((sess: any) => \`<div class="metric"><b>\${(sess.id||'sess').slice(0,12)}</b> \${sess.status||''}</div>\`).join('') : '<div class="small">无活跃会话</div>';
271
- } catch(e){}
107
+ async handleRequest(req, res) {
108
+ this.setSecurityHeaders(res);
109
+ if (!this.sameOriginAllowed(req)) {
110
+ sendJson(res, 403, { error: "cross-origin dashboard request denied" });
111
+ return;
112
+ }
113
+ if (req.method === "OPTIONS") {
114
+ res.writeHead(204);
115
+ res.end();
116
+ return;
117
+ }
118
+ const url = new URL(req.url || "/", `http://127.0.0.1:${this.options.port}`);
119
+ try {
120
+ if (url.pathname === "/" && req.method === "GET") {
121
+ res.writeHead(200, {
122
+ "Content-Type": "text/html; charset=utf-8",
123
+ "Cache-Control": "no-cache",
124
+ });
125
+ res.end(DASHBOARD_HTML);
126
+ return;
127
+ }
128
+ if (url.pathname === "/assets/dashboard.css" && req.method === "GET") {
129
+ res.writeHead(200, {
130
+ "Content-Type": "text/css; charset=utf-8",
131
+ "Cache-Control": "public, max-age=3600",
132
+ });
133
+ res.end(DASHBOARD_CSS);
134
+ return;
135
+ }
136
+ if (url.pathname === "/assets/dashboard.js" && req.method === "GET") {
137
+ this.clientSource ??= await readFile(CLIENT_URL, "utf-8");
138
+ res.writeHead(200, {
139
+ "Content-Type": "text/javascript; charset=utf-8",
140
+ "Cache-Control": "no-cache",
141
+ });
142
+ res.end(this.clientSource);
143
+ return;
144
+ }
145
+ if (url.pathname === "/api/dashboard/snapshot" && req.method === "GET") {
146
+ await this.serveSnapshot(req, res);
147
+ return;
148
+ }
149
+ const taskRoute = parseTaskRoute(url.pathname);
150
+ if (taskRoute && req.method === "GET" && !taskRoute.action) {
151
+ sendJson(res, 200, await this.buildTaskDetail(taskRoute.kind, taskRoute.id));
152
+ return;
153
+ }
154
+ if (taskRoute && req.method === "POST" && taskRoute.action) {
155
+ const result = await this.controlTask(taskRoute.kind, taskRoute.id, taskRoute.action);
156
+ sendJson(res, result.status, result.body);
157
+ return;
158
+ }
159
+ if (url.pathname === "/api/metrics" && req.method === "GET") {
160
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 100));
161
+ const recent = await this.options.collector.queryRecent({ limit });
162
+ sendJson(res, 200, recent.events);
163
+ return;
164
+ }
165
+ if (url.pathname === "/api/status" && req.method === "GET") {
166
+ sendJson(res, 200, {
167
+ checkpoint: this.options.workflowRuntime.getCheckpoint(),
168
+ is_running: this.options.agentLoop.turnCount > 0,
169
+ session_id: this.getSessionId(),
170
+ });
171
+ return;
172
+ }
173
+ if (url.pathname === "/api/missions" && req.method === "GET") {
174
+ const manager = this.options.agentLoop.getMissionManager();
175
+ await manager.refresh();
176
+ sendJson(res, 200, manager.listMissions());
177
+ return;
178
+ }
179
+ if (url.pathname === "/api/sessions" && req.method === "GET") {
180
+ sendJson(res, 200, { sessions: [{ id: this.getSessionId(), status: "active" }] });
181
+ return;
182
+ }
183
+ if (url.pathname === "/api/permissions" && req.method === "GET") {
184
+ sendJson(res, 200, this.permissionsReport());
185
+ return;
186
+ }
187
+ if (url.pathname === "/api/doctor" && req.method === "GET") {
188
+ sendJson(res, 200, {
189
+ status: "ok",
190
+ node: process.version,
191
+ dashboard: true,
192
+ daemonHealthy: this.daemonHealthy,
193
+ note: "使用 /doctor 命令获取完整本地诊断",
194
+ });
195
+ return;
196
+ }
197
+ if (url.pathname === "/api/control/pause" && req.method === "POST") {
198
+ this.options.agentLoop.emit("control_signal", "pause");
199
+ sendJson(res, 200, { ok: true, action: "pause", status: "pausing" });
200
+ return;
201
+ }
202
+ if (url.pathname === "/api/control/resume" && req.method === "POST") {
203
+ this.options.agentLoop.emit("control_signal", "resume");
204
+ sendJson(res, 200, { ok: true, action: "resume", status: "resuming" });
205
+ return;
206
+ }
207
+ if (url.pathname.startsWith("/api/")) {
208
+ sendJson(res, 404, { error: `dashboard route not found: ${url.pathname}` });
209
+ return;
210
+ }
211
+ sendJson(res, 404, { error: "not found" });
212
+ }
213
+ catch (err) {
214
+ const status = Number(err.statusCode) || 500;
215
+ sendJson(res, status, { error: err instanceof Error ? err.message : String(err) });
216
+ }
272
217
  }
273
-
274
- async function loadDoctor() {
275
- try {
276
- const d = await fetchJSON('api/doctor');
277
- let el = document.getElementById('doctor-panel');
278
- if (!el) return;
279
- el.innerHTML = \`<b>状态</b>: \${d.status || 'ok'} <span class="pill ok">本地</span>\`;
280
- } catch(e){}
218
+ async serveSnapshot(req, res) {
219
+ const cache = await this.getSnapshot();
220
+ if (req.headers["if-none-match"] === cache.etag) {
221
+ res.writeHead(304, { ETag: cache.etag, "Cache-Control": "no-cache" });
222
+ res.end();
223
+ return;
224
+ }
225
+ res.writeHead(200, {
226
+ ...JSON_HEADERS,
227
+ ETag: cache.etag,
228
+ "Cache-Control": "no-cache",
229
+ });
230
+ res.end(cache.payload);
281
231
  }
282
-
283
- async function refreshAll() {
284
- await Promise.all([loadStatus(), loadMissions(), loadMetrics(), loadPerm(), loadSessions(), loadDoctor()]);
232
+ async getSnapshot() {
233
+ const now = Date.now();
234
+ if (this.snapshotCache && this.snapshotCache.expiresAt > now)
235
+ return this.snapshotCache;
236
+ const manager = this.options.agentLoop.getMissionManager();
237
+ await manager.refresh();
238
+ const loopsReport = await listLocalSessionTasks(this.getStateDir(), {
239
+ count: TASK_SCAN_LIMIT,
240
+ maxCount: TASK_SCAN_LIMIT,
241
+ });
242
+ const recent = await this.options.collector.queryRecent({
243
+ limit: ACTIVITY_LIMIT,
244
+ maxScanBytes: 1024 * 1024,
245
+ });
246
+ const allTasks = buildDashboardTasks({
247
+ missions: manager.listMissions(),
248
+ loops: loopsReport.tasks,
249
+ workflow: this.options.workflowRuntime.getCheckpoint(),
250
+ daemonHealthy: this.daemonHealthy,
251
+ now,
252
+ });
253
+ const tasks = allTasks.slice(0, TASK_LIMIT);
254
+ const stable = {
255
+ runtime: {
256
+ ready: true,
257
+ sessionId: this.getSessionId(),
258
+ daemonHealthy: this.daemonHealthy,
259
+ daemonSource: this.daemonHealthy ? "daemon" : "local",
260
+ permissionMode: this.getPermissionMode(),
261
+ },
262
+ summary: summaryFor(allTasks),
263
+ tasks,
264
+ activity: recent.events,
265
+ boundary: {
266
+ localOnly: true,
267
+ activityTruncated: recent.truncated,
268
+ activityScannedBytes: recent.scannedBytes,
269
+ },
270
+ };
271
+ const revision = createHash("sha256").update(JSON.stringify(stable)).digest("hex").slice(0, 16);
272
+ const snapshot = { generatedAt: now, revision, ...stable };
273
+ const payload = JSON.stringify(snapshot);
274
+ this.snapshotCache = {
275
+ snapshot,
276
+ payload,
277
+ etag: `"${revision}"`,
278
+ expiresAt: now + SNAPSHOT_CACHE_MS,
279
+ };
280
+ return this.snapshotCache;
285
281
  }
286
-
287
- async function pauseMission() {
288
- try { await fetch('/api/control/pause', {method:'POST'}); alert('暂停信号已发送'); refreshAll(); } catch(e){}
282
+ async buildTaskDetail(kind, id) {
283
+ if (kind === "mission") {
284
+ const manager = this.options.agentLoop.getMissionManager();
285
+ await manager.refresh();
286
+ const mission = manager.getMissionOrThrow(id);
287
+ const task = buildDashboardTasks({ missions: [mission], loops: [], workflow: null, daemonHealthy: this.daemonHealthy })[0];
288
+ return { task, detail: mission, events: await manager.getMissionLogs(id) };
289
+ }
290
+ if (kind === "loop") {
291
+ const report = await listLocalSessionTasks(this.getStateDir(), {
292
+ count: TASK_SCAN_LIMIT,
293
+ maxCount: TASK_SCAN_LIMIT,
294
+ });
295
+ const loop = report.tasks.find((task) => task.id === id);
296
+ if (!loop)
297
+ throw Object.assign(new Error(`session task not found: ${id}`), { statusCode: 404 });
298
+ const task = buildDashboardTasks({ missions: [], loops: [loop], workflow: null, daemonHealthy: this.daemonHealthy })[0];
299
+ const trace = task.sessionId ? await this.options.agentLoop.getRecentRunTrace(task.sessionId) : [];
300
+ return { task, detail: loop, events: trace };
301
+ }
302
+ const workflow = this.options.workflowRuntime.getCheckpoint();
303
+ if (!workflow || workflow.runId !== id) {
304
+ throw Object.assign(new Error(`workflow not found: ${id}`), { statusCode: 404 });
305
+ }
306
+ const task = buildDashboardTasks({ missions: [], loops: [], workflow, daemonHealthy: this.daemonHealthy })[0];
307
+ const trace = await this.options.agentLoop.getRecentRunTrace(this.getSessionId());
308
+ return {
309
+ task,
310
+ detail: workflow,
311
+ events: [...workflow.history, ...trace],
312
+ };
289
313
  }
290
- async function resumeMission() {
291
- try { await fetch('/api/control/resume', {method:'POST'}); alert('恢复信号已发送'); refreshAll(); } catch(e){}
314
+ async controlTask(kind, id, action) {
315
+ if (kind === "workflow") {
316
+ return { status: 405, body: { ok: false, source: "local", message: "Workflow 在 Dashboard 中仅供查看" } };
317
+ }
318
+ if (kind === "loop") {
319
+ if (action !== "cancel")
320
+ return { status: 405, body: { ok: false, source: "local", message: "Loop task 仅支持 cancel" } };
321
+ const loop = await cancelLocalSessionTask(this.getStateDir(), id);
322
+ this.invalidateSnapshot();
323
+ const task = buildDashboardTasks({ missions: [], loops: [loop], workflow: null, daemonHealthy: this.daemonHealthy })[0];
324
+ return { status: 200, body: { ok: true, source: "local", task, message: `已取消循环任务 ${id}` } };
325
+ }
326
+ if (!["pause", "resume", "cancel", "retry"].includes(action)) {
327
+ return { status: 405, body: { ok: false, source: "local", message: `Mission 不支持操作 ${action}` } };
328
+ }
329
+ if (action === "retry" && !this.daemonHealthy) {
330
+ return { status: 503, body: { ok: false, source: "local", message: "重试需要运行中的 qling daemon;请先执行 qling daemon start" } };
331
+ }
332
+ if (this.daemonHealthy) {
333
+ const remote = await this.controlMissionViaDaemon(id, action);
334
+ if (remote)
335
+ return remote;
336
+ if (action === "retry") {
337
+ return { status: 503, body: { ok: false, source: "local", message: "Daemon 已离线,未创建无法执行的重试任务" } };
338
+ }
339
+ }
340
+ const manager = this.options.agentLoop.getMissionManager();
341
+ await manager.refresh();
342
+ let mission;
343
+ if (action === "pause")
344
+ mission = await manager.pauseMission(id, "dashboard_local");
345
+ else if (action === "resume")
346
+ mission = await manager.resumeMission(id, "dashboard_local");
347
+ else
348
+ mission = await manager.cancelMission(id, "dashboard_local");
349
+ this.invalidateSnapshot();
350
+ const task = buildDashboardTasks({ missions: [mission], loops: [], workflow: null, daemonHealthy: false })[0];
351
+ return { status: 200, body: { ok: true, source: "local", task, message: `已在本地状态中${this.actionLabel(action)} ${mission.name}` } };
292
352
  }
293
-
294
- // 启动
295
- refreshAll();
296
- setInterval(refreshAll, 15000); // 15s 自动刷新
297
- </script>
298
- </body>
299
- </html>`);
300
- }
301
- catch (err) {
302
- res.writeHead(500);
303
- res.end(JSON.stringify({ error: err.message }));
304
- }
305
- });
306
- this.server.on("error", (err) => {
307
- if (err.code === "EADDRINUSE") {
308
- reject(new Error(`EADDRINUSE: 端口 ${this.options.port} 已被占用`));
309
- }
310
- else {
311
- reject(err);
312
- }
313
- });
314
- this.server.listen(this.options.port, () => {
315
- console.error(`🚀 Dashboard 运行在: http://localhost:${this.options.port}`);
316
- this.listening = true;
317
- resolve();
353
+ async controlMissionViaDaemon(id, action) {
354
+ try {
355
+ const response = await fetch(`${this.daemonUrl()}/missions/${encodeURIComponent(id)}/${action}`, {
356
+ method: "POST",
357
+ signal: AbortSignal.timeout(1_200),
318
358
  });
359
+ if (!response.ok) {
360
+ const body = await response.json().catch(() => ({}));
361
+ return { status: response.status, body: { ok: false, source: "daemon", message: body.error ?? `Daemon 操作失败 (${response.status})` } };
362
+ }
363
+ const body = await response.json();
364
+ const manager = this.options.agentLoop.getMissionManager();
365
+ await manager.refresh();
366
+ const mission = body.mission ?? manager.getMission(body.missionId ?? id);
367
+ const task = mission ? buildDashboardTasks({ missions: [mission], loops: [], workflow: null, daemonHealthy: true })[0] : undefined;
368
+ this.invalidateSnapshot();
369
+ return { status: 200, body: { ok: true, source: "daemon", task, message: `Daemon 已${this.actionLabel(action)}任务` } };
370
+ }
371
+ catch {
372
+ this.daemonHealthy = false;
373
+ this.invalidateSnapshot();
374
+ return null;
375
+ }
376
+ }
377
+ async probeDaemon() {
378
+ try {
379
+ const response = await fetch(`${this.daemonUrl()}/health`, { signal: AbortSignal.timeout(350) });
380
+ const healthy = response.ok;
381
+ if (healthy !== this.daemonHealthy) {
382
+ this.daemonHealthy = healthy;
383
+ this.invalidateSnapshot();
384
+ }
385
+ }
386
+ catch {
387
+ if (this.daemonHealthy) {
388
+ this.daemonHealthy = false;
389
+ this.invalidateSnapshot();
390
+ }
391
+ }
392
+ }
393
+ permissionsReport() {
394
+ const config = this.options.agentLoop.config || {};
395
+ return buildLocalPermissionsReport({
396
+ defaultMode: config.guard?.permissions?.default || this.getPermissionMode(),
397
+ rules: config.guard?.permissions?.rules || [],
398
+ env: process.env,
319
399
  });
320
400
  }
321
- stop() {
322
- this.server?.close();
323
- this.listening = false;
401
+ getStateDir() {
402
+ return this.options.agentLoop.getRuntimeRootDir();
403
+ }
404
+ getSessionId() {
405
+ return this.options.agentLoop.getSessionId();
406
+ }
407
+ getPermissionMode() {
408
+ return this.options.agentLoop.getPermissionMode?.() ?? "ask";
409
+ }
410
+ daemonUrl() {
411
+ const port = Number(process.env.QLING_DAEMON_PORT) || 9998;
412
+ return `http://127.0.0.1:${port}`;
413
+ }
414
+ actionLabel(action) {
415
+ return { pause: "暂停", resume: "恢复", cancel: "取消", retry: "重试" }[action] ?? action;
416
+ }
417
+ invalidateSnapshot() {
418
+ this.snapshotCache = null;
324
419
  }
325
420
  }