@tea-agent/loop-agent 0.33.0 → 0.33.2
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/CHANGELOG.md +44 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/{index-3R-GT3a_.js → index-PzYzcuFG.js} +17 -17
- package/dist/worker/console/static/index.html +3 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +3 -2
- package/dist/worker/observe/routes.js +3 -8
- package/dist/worker/observe/static/app.js +11 -0
- package/dist/worker/observe/static/index.html +11 -43
- package/dist/worker/observe/static/kpi.js +2 -24
- package/dist/worker/observe/static/operator-chrome.css +480 -0
- package/dist/worker/observe/static/operator-chrome.d.ts +82 -0
- package/dist/worker/observe/static/operator-chrome.js +554 -0
- package/dist/worker/observe/static/shell-chrome.js +1 -11
- package/dist/worker/observe/static/styles.css +62 -269
- package/dist/worker/observe/static/views/dashboard.js +52 -4
- package/dist/workflows/dag/retry-policy.js +5 -0
- package/package.json +1 -1
- package/skills/analyze-product-dependencies/SKILL.md +74 -33
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +16 -11
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +20 -10
- package/skills/analyze-product-dependencies/references/example.md +9 -9
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +93 -18
- package/skills/analyze-product-dependencies/references/input-contract.md +27 -4
- package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +25 -10
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +247 -54
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +122 -29
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +94 -36
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +37 -23
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +35 -43
- package/skills/analyze-product-requirements/SKILL.md +98 -43
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +8 -12
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +28 -14
- package/skills/analyze-product-requirements/references/example.md +24 -6
- package/skills/analyze-product-requirements/references/forward-test-cases.md +87 -9
- package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +19 -10
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +21 -12
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +45 -14
- package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +337 -29
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +38 -7
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +41 -29
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +33 -22
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +43 -24
- package/dist/worker/console/static/assets/index-B0EQt_yq.css +0 -1
- package/skills/analyze-product-dependencies/agents/openai.yaml +0 -4
- package/skills/analyze-product-requirements/agents/openai.yaml +0 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** 与 operator-chrome.js 对应的类型声明(供 vitest/TS 侧导入契约测试)。 */
|
|
2
|
+
|
|
3
|
+
export type OperatorChromeTab = { id: string; label: string; href: string };
|
|
4
|
+
|
|
5
|
+
export const OPERATE_TABS: readonly OperatorChromeTab[];
|
|
6
|
+
export const INSPECT_TABS: readonly OperatorChromeTab[];
|
|
7
|
+
export const READINESS_LABELS: Record<string, string>;
|
|
8
|
+
|
|
9
|
+
export type InspectObjectView = { view: string; id: string };
|
|
10
|
+
|
|
11
|
+
export type InspectContext = {
|
|
12
|
+
view: string;
|
|
13
|
+
id: string;
|
|
14
|
+
from: string;
|
|
15
|
+
kindLabel: string;
|
|
16
|
+
returnHref: string;
|
|
17
|
+
returnLabel: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ChromeRepoInfo = {
|
|
21
|
+
name: string;
|
|
22
|
+
fingerprint: string;
|
|
23
|
+
version: string;
|
|
24
|
+
product: string;
|
|
25
|
+
inspectReady?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ChromeModel = {
|
|
29
|
+
mode: "operate" | "inspect";
|
|
30
|
+
homeHref: string;
|
|
31
|
+
brand: string;
|
|
32
|
+
repo: ChromeRepoInfo | null;
|
|
33
|
+
modeButtons: Array<{
|
|
34
|
+
id: "operate" | "inspect";
|
|
35
|
+
label: string;
|
|
36
|
+
href: string;
|
|
37
|
+
active: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
context: InspectContext | null;
|
|
40
|
+
freshness: boolean;
|
|
41
|
+
pi: { state: string; label: string; href: string; title: string };
|
|
42
|
+
tabs: Array<OperatorChromeTab & { active: boolean }>;
|
|
43
|
+
tabsAriaLabel: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function parseInspectObjectView(
|
|
47
|
+
hashValue: string,
|
|
48
|
+
): InspectObjectView | null;
|
|
49
|
+
export function activeInspectTabHref(hashValue: string): string | null;
|
|
50
|
+
export function parseInspectContext(hashValue: string): InspectContext | null;
|
|
51
|
+
export function resolveOperateHrefFromInspectHash(hashValue: string): string;
|
|
52
|
+
export function buildInspectDagHref(
|
|
53
|
+
dagRunId: string,
|
|
54
|
+
options?: { from?: string },
|
|
55
|
+
): string;
|
|
56
|
+
export function clearInspectContextSource(hashValue: string): string;
|
|
57
|
+
export function repoBaseName(repoRoot: string): string;
|
|
58
|
+
export function shortFingerprint(fingerprint: string): string;
|
|
59
|
+
export function chromeStateFromHealth(body: unknown): {
|
|
60
|
+
readiness: string;
|
|
61
|
+
readinessLabel: string;
|
|
62
|
+
repo: ChromeRepoInfo;
|
|
63
|
+
};
|
|
64
|
+
export function buildOperatorChromeModel(config: {
|
|
65
|
+
mode: "operate" | "inspect";
|
|
66
|
+
activeTabHref?: string | null;
|
|
67
|
+
operateHref?: string;
|
|
68
|
+
inspectHref?: string;
|
|
69
|
+
readinessState?: string;
|
|
70
|
+
readinessHref?: string;
|
|
71
|
+
readinessTitle?: string;
|
|
72
|
+
repo?: ChromeRepoInfo | null;
|
|
73
|
+
freshness?: boolean;
|
|
74
|
+
context?: InspectContext | null;
|
|
75
|
+
}): ChromeModel;
|
|
76
|
+
export function mountOperatorChrome(
|
|
77
|
+
root: HTMLElement,
|
|
78
|
+
config?: Record<string, unknown>,
|
|
79
|
+
): {
|
|
80
|
+
update: (nextConfig?: Record<string, unknown>) => void;
|
|
81
|
+
destroy: () => void;
|
|
82
|
+
};
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chrome — 统一壳(Operate React 与 Inspect vanilla 共用唯一实现)。
|
|
3
|
+
*
|
|
4
|
+
* 设计真源:docs/design/active/2026-08-09-unified-operator-chrome.md
|
|
5
|
+
* - 一条 chrome 两条带:Band1 全局(品牌/仓库身份/模式分段控件/对象上下文/状态格),
|
|
6
|
+
* Band2 工作区(下划线 tab,随模式切换内容)。
|
|
7
|
+
* - 模式语义:操作=做,观测=看同一对象;模式切换在持有 DAG 选中时不丢对象。
|
|
8
|
+
* - Inspect 永不提供写入口;需要动作时跳回操作侧。
|
|
9
|
+
*
|
|
10
|
+
* 纯函数(parse/build/resolve)不触碰 DOM,node 侧可直接单测;
|
|
11
|
+
* mountOperatorChrome 仅在浏览器环境调用。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const OPERATE_TABS = Object.freeze([
|
|
15
|
+
{ id: "chat", label: "对话", href: "#/chat" },
|
|
16
|
+
{ id: "tasks", label: "任务", href: "#/tasks" },
|
|
17
|
+
{ id: "recovery", label: "恢复", href: "#/recovery" },
|
|
18
|
+
{ id: "night", label: "夜间", href: "#/night" },
|
|
19
|
+
{ id: "models", label: "模型与认证", href: "#/models" },
|
|
20
|
+
{ id: "settings", label: "设置", href: "#/settings" },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export const INSPECT_TABS = Object.freeze([
|
|
24
|
+
{ id: "dashboard", label: "总览", href: "#/" },
|
|
25
|
+
{ id: "pool", label: "资源池", href: "#/pool" },
|
|
26
|
+
{ id: "failures", label: "异常 Task", href: "#/failures" },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const READINESS_LABELS = Object.freeze({
|
|
30
|
+
"setup-required": "需要设置",
|
|
31
|
+
ready: "已就绪",
|
|
32
|
+
degraded: "降级可用",
|
|
33
|
+
unknown: "未知",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const CONTEXT_KIND_LABELS = Object.freeze({
|
|
37
|
+
dag: "DAG",
|
|
38
|
+
run: "Run",
|
|
39
|
+
task: "Task",
|
|
40
|
+
feature: "Feature",
|
|
41
|
+
batch: "Batch",
|
|
42
|
+
night: "夜间",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** 支持返回操作侧的对象视图 → 返回落地工作区。v1 仅接通 recovery 闭环。 */
|
|
46
|
+
const RETURNABLE_VIEWS = new Set(["dag", "run"]);
|
|
47
|
+
|
|
48
|
+
function splitHash(hashValue) {
|
|
49
|
+
const raw = String(hashValue ?? "").replace(/^#/, "");
|
|
50
|
+
const qIndex = raw.indexOf("?");
|
|
51
|
+
const path = (qIndex >= 0 ? raw.slice(0, qIndex) : raw) || "/";
|
|
52
|
+
const query = qIndex >= 0 ? new URLSearchParams(raw.slice(qIndex + 1)) : null;
|
|
53
|
+
return { path, query };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function decode(value) {
|
|
57
|
+
try {
|
|
58
|
+
return decodeURIComponent(value);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 与 router.js 同形的最小对象视图解析(只读 chrome 用,不替代路由)。 */
|
|
65
|
+
export function parseInspectObjectView(hashValue) {
|
|
66
|
+
const { path } = splitHash(hashValue);
|
|
67
|
+
const match = /^\/(dag|run|task|feature|batch|night)\/([^/]+)$/.exec(path);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
const id = decode(match[2]);
|
|
70
|
+
if (!id) return null;
|
|
71
|
+
return { view: match[1], id };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Inspect 当前 hash 应高亮的工作区 tab href。
|
|
76
|
+
* 三个一级集合视图映射到各自 tab;下钻/对象页(dag/dags/run/night 等)
|
|
77
|
+
* 不高亮任何 tab——位置由面包屑与上下文 chip 表达(`#/dags` 自 0.33.0
|
|
78
|
+
* 起是独立历史分页页,不再是总览滚动别名)。
|
|
79
|
+
*/
|
|
80
|
+
export function activeInspectTabHref(hashValue) {
|
|
81
|
+
const { path } = splitHash(hashValue);
|
|
82
|
+
if (path === "/" || path === "") return "#/";
|
|
83
|
+
if (path === "/pool" || path.startsWith("/task/")) return "#/pool";
|
|
84
|
+
if (path === "/failures") return "#/failures";
|
|
85
|
+
if (path.startsWith("/feature/")) return "#/";
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 对象上下文 chip 的选中模型。仅当 URL 携带 from 来源(如 from=recovery)
|
|
91
|
+
* 且当前为可返回对象视图时返回;否则为 null(chip 不出现)。
|
|
92
|
+
*/
|
|
93
|
+
export function parseInspectContext(hashValue) {
|
|
94
|
+
const objectView = parseInspectObjectView(hashValue);
|
|
95
|
+
if (!objectView || !RETURNABLE_VIEWS.has(objectView.view)) return null;
|
|
96
|
+
const { query } = splitHash(hashValue);
|
|
97
|
+
const from = query?.get("from") ?? null;
|
|
98
|
+
if (from !== "recovery") return null;
|
|
99
|
+
const returnParams = new URLSearchParams();
|
|
100
|
+
if (objectView.view === "dag") returnParams.set("dagRunId", objectView.id);
|
|
101
|
+
const suffix = returnParams.toString();
|
|
102
|
+
return {
|
|
103
|
+
view: objectView.view,
|
|
104
|
+
id: objectView.id,
|
|
105
|
+
from,
|
|
106
|
+
kindLabel: CONTEXT_KIND_LABELS[objectView.view] ?? objectView.view,
|
|
107
|
+
returnHref: `/#/recovery${suffix ? `?${suffix}` : ""}`,
|
|
108
|
+
returnLabel: "返回 恢复",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 从 Inspect 切回操作侧:持有 DAG 选中时不丢对象(换动作不换对象)。 */
|
|
113
|
+
export function resolveOperateHrefFromInspectHash(hashValue) {
|
|
114
|
+
const objectView = parseInspectObjectView(hashValue);
|
|
115
|
+
if (objectView?.view === "dag") {
|
|
116
|
+
return `/#/recovery?dagRunId=${encodeURIComponent(objectView.id)}`;
|
|
117
|
+
}
|
|
118
|
+
return "/#/";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 操作侧 → Inspect 的 canonical 深链(可携带 from 以激活上下文 chip)。 */
|
|
122
|
+
export function buildInspectDagHref(dagRunId, options = {}) {
|
|
123
|
+
const base = `/inspect/#/dag/${encodeURIComponent(dagRunId)}`;
|
|
124
|
+
return options.from === "recovery" ? `${base}?from=recovery` : base;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 清除跨模式来源标记,但保留对象页上的其它查询参数与当前历史位置。 */
|
|
128
|
+
export function clearInspectContextSource(hashValue) {
|
|
129
|
+
const { path, query } = splitHash(hashValue);
|
|
130
|
+
const nextQuery = new URLSearchParams(query ?? undefined);
|
|
131
|
+
nextQuery.delete("from");
|
|
132
|
+
const suffix = nextQuery.toString();
|
|
133
|
+
return `#${path}${suffix ? `?${suffix}` : ""}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function repoBaseName(repoRoot) {
|
|
137
|
+
const parts = String(repoRoot ?? "")
|
|
138
|
+
.split(/[\\/]/)
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
return parts.at(-1) ?? "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function shortFingerprint(fingerprint) {
|
|
144
|
+
const raw = String(fingerprint ?? "").trim();
|
|
145
|
+
return raw ? raw.slice(0, 6) : "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 统一/兼容 health 负载 → chrome 状态槽。缺字段时降级为 unknown。 */
|
|
149
|
+
export function chromeStateFromHealth(body) {
|
|
150
|
+
const health = body && typeof body === "object" ? body : {};
|
|
151
|
+
const readiness =
|
|
152
|
+
typeof health.piReadiness === "string" ? health.piReadiness : "unknown";
|
|
153
|
+
return {
|
|
154
|
+
readiness,
|
|
155
|
+
readinessLabel: READINESS_LABELS[readiness] ?? readiness,
|
|
156
|
+
repo: {
|
|
157
|
+
name: health.repoName || repoBaseName(health.repoRoot) || "repository",
|
|
158
|
+
fingerprint: health.repoFingerprint ?? "",
|
|
159
|
+
version: health.packageVersion ?? "",
|
|
160
|
+
product: health.productName ?? health.product ?? "",
|
|
161
|
+
inspectReady: health.inspect?.ready,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 统一 chrome 的声明式模型。Operate(React 包装)与 Inspect(vanilla 挂载)
|
|
168
|
+
* 共用同一份模型结构,保证 DOM 契约一致。
|
|
169
|
+
*/
|
|
170
|
+
export function buildOperatorChromeModel(config) {
|
|
171
|
+
const mode = config.mode === "inspect" ? "inspect" : "operate";
|
|
172
|
+
const tabs = (mode === "inspect" ? INSPECT_TABS : OPERATE_TABS).map(
|
|
173
|
+
(tab) => ({
|
|
174
|
+
...tab,
|
|
175
|
+
active: tab.href === (config.activeTabHref ?? null),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
const state = config.readinessState ?? "unknown";
|
|
179
|
+
return {
|
|
180
|
+
mode,
|
|
181
|
+
homeHref: mode === "inspect" ? "/inspect/#/" : "/#/",
|
|
182
|
+
brand: "Loop Agent",
|
|
183
|
+
repo: config.repo ?? null,
|
|
184
|
+
modeButtons: [
|
|
185
|
+
{
|
|
186
|
+
id: "operate",
|
|
187
|
+
label: "操作",
|
|
188
|
+
href: config.operateHref ?? "/#/",
|
|
189
|
+
active: mode === "operate",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "inspect",
|
|
193
|
+
label: "观测",
|
|
194
|
+
href: config.inspectHref ?? "/inspect/#/",
|
|
195
|
+
active: mode === "inspect",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
context: config.context ?? null,
|
|
199
|
+
freshness: mode === "inspect" && config.freshness !== false,
|
|
200
|
+
pi: {
|
|
201
|
+
state,
|
|
202
|
+
label: READINESS_LABELS[state] ?? state,
|
|
203
|
+
href:
|
|
204
|
+
config.readinessHref ?? (mode === "inspect" ? "/#/models" : "#/models"),
|
|
205
|
+
title: config.readinessTitle ?? "",
|
|
206
|
+
},
|
|
207
|
+
tabs,
|
|
208
|
+
tabsAriaLabel: mode === "inspect" ? "观测看板" : "操作工作区",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ---------------- 以下为浏览器 DOM 挂载层 ---------------- */
|
|
213
|
+
|
|
214
|
+
const SVG_NS = "http:" + "//www.w3.org/2000/svg";
|
|
215
|
+
const ICONS = Object.freeze({
|
|
216
|
+
radar: {
|
|
217
|
+
size: 17,
|
|
218
|
+
parts: [
|
|
219
|
+
["circle", { cx: 12, cy: 12, r: 9 }],
|
|
220
|
+
["circle", { cx: 12, cy: 12, r: 4.5 }],
|
|
221
|
+
["path", { d: "M12 12 L18 6" }],
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
zap: {
|
|
225
|
+
size: 13,
|
|
226
|
+
join: "round",
|
|
227
|
+
parts: [["path", { d: "M13 2 4.5 13.5H11L10 22l8.5-11.5H12L13 2z" }]],
|
|
228
|
+
},
|
|
229
|
+
eye: {
|
|
230
|
+
size: 13,
|
|
231
|
+
parts: [
|
|
232
|
+
[
|
|
233
|
+
"path",
|
|
234
|
+
{
|
|
235
|
+
d: "M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z",
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
["circle", { cx: 12, cy: 12, r: 2.8 }],
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
branch: {
|
|
242
|
+
size: 13,
|
|
243
|
+
parts: [
|
|
244
|
+
["circle", { cx: 6, cy: 6, r: 2.4 }],
|
|
245
|
+
["circle", { cx: 6, cy: 18, r: 2.4 }],
|
|
246
|
+
["circle", { cx: 18, cy: 8, r: 2.4 }],
|
|
247
|
+
["path", { d: "M6 8.4v7.2M8.2 6.9 15.8 8" }],
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
chevron: {
|
|
251
|
+
size: 10,
|
|
252
|
+
strokeWidth: 2.4,
|
|
253
|
+
parts: [["path", { d: "m6 9 6 6 6-6" }]],
|
|
254
|
+
},
|
|
255
|
+
arrowLeft: {
|
|
256
|
+
size: 11,
|
|
257
|
+
strokeWidth: 2.4,
|
|
258
|
+
join: "round",
|
|
259
|
+
parts: [["path", { d: "M19 12H5m6-7-7 7 7 7" }]],
|
|
260
|
+
},
|
|
261
|
+
close: {
|
|
262
|
+
size: 10,
|
|
263
|
+
strokeWidth: 2.4,
|
|
264
|
+
parts: [["path", { d: "M18 6 6 18M6 6l12 12" }]],
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
function el(tag, className, text) {
|
|
269
|
+
const node = document.createElement(tag);
|
|
270
|
+
if (className) node.className = className;
|
|
271
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
272
|
+
return node;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function makeIcon(name) {
|
|
276
|
+
const span = el("span", "och-icon");
|
|
277
|
+
const definition = ICONS[name];
|
|
278
|
+
if (definition) {
|
|
279
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
280
|
+
svg.setAttribute("width", String(definition.size));
|
|
281
|
+
svg.setAttribute("height", String(definition.size));
|
|
282
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
283
|
+
svg.setAttribute("fill", "none");
|
|
284
|
+
svg.setAttribute("stroke", "currentColor");
|
|
285
|
+
svg.setAttribute("stroke-width", String(definition.strokeWidth ?? 2));
|
|
286
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
287
|
+
if (definition.join) svg.setAttribute("stroke-linejoin", definition.join);
|
|
288
|
+
svg.setAttribute("aria-hidden", "true");
|
|
289
|
+
for (const [tag, attributes] of definition.parts) {
|
|
290
|
+
const part = document.createElementNS(SVG_NS, tag);
|
|
291
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
292
|
+
part.setAttribute(key, String(value));
|
|
293
|
+
}
|
|
294
|
+
svg.appendChild(part);
|
|
295
|
+
}
|
|
296
|
+
span.appendChild(svg);
|
|
297
|
+
}
|
|
298
|
+
span.style.display = "inline-flex";
|
|
299
|
+
return span;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function icon(parent, name) {
|
|
303
|
+
const span = makeIcon(name);
|
|
304
|
+
parent.appendChild(span);
|
|
305
|
+
return span;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let chromeInstanceSequence = 0;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 挂载统一壳。config 见 buildOperatorChromeModel;另外支持:
|
|
312
|
+
* - healthUrl:提供则挂载后拉取一次,填充仓库身份与 Pi 状态(Inspect 侧用)。
|
|
313
|
+
* - liveLocation:提供则监听 hashchange,自选高亮 tab、自更新 operateHref 与
|
|
314
|
+
* 上下文 chip(Inspect 侧用;Operate 侧由 React 包裹层驱动 update)。
|
|
315
|
+
*/
|
|
316
|
+
export function mountOperatorChrome(root, config = {}) {
|
|
317
|
+
if (!root) throw new Error("mountOperatorChrome: root element is required");
|
|
318
|
+
root.classList.add("och-host");
|
|
319
|
+
|
|
320
|
+
let currentConfig = { ...config };
|
|
321
|
+
let popoverOpen = false;
|
|
322
|
+
let healthState = null;
|
|
323
|
+
let destroyed = false;
|
|
324
|
+
let freshnessText = "";
|
|
325
|
+
const popoverId = `och-repo-popover-${++chromeInstanceSequence}`;
|
|
326
|
+
|
|
327
|
+
function currentModel() {
|
|
328
|
+
const base = { ...currentConfig };
|
|
329
|
+
if (base.liveLocation) {
|
|
330
|
+
const hash = window.location.hash;
|
|
331
|
+
base.activeTabHref = activeInspectTabHref(hash);
|
|
332
|
+
base.operateHref = resolveOperateHrefFromInspectHash(hash);
|
|
333
|
+
base.context = parseInspectContext(hash);
|
|
334
|
+
}
|
|
335
|
+
if (healthState) {
|
|
336
|
+
base.readinessState = healthState.readiness;
|
|
337
|
+
base.repo = healthState.repo;
|
|
338
|
+
}
|
|
339
|
+
return buildOperatorChromeModel(base);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function build(model) {
|
|
343
|
+
freshnessText =
|
|
344
|
+
root.querySelector("#header-refresh")?.textContent ?? freshnessText;
|
|
345
|
+
root.textContent = "";
|
|
346
|
+
const chrome = el("div", "och");
|
|
347
|
+
const inner = el("div", "och-inner");
|
|
348
|
+
chrome.appendChild(inner);
|
|
349
|
+
|
|
350
|
+
/* Band 1 */
|
|
351
|
+
const global = el("div", "och-global");
|
|
352
|
+
inner.appendChild(global);
|
|
353
|
+
|
|
354
|
+
const brand = el("a", "och-brand");
|
|
355
|
+
brand.href = model.homeHref;
|
|
356
|
+
icon(brand, "radar");
|
|
357
|
+
brand.appendChild(el("span", null, model.brand));
|
|
358
|
+
global.appendChild(brand);
|
|
359
|
+
|
|
360
|
+
if (model.repo) {
|
|
361
|
+
const repoBtn = el("button", "och-repo");
|
|
362
|
+
repoBtn.type = "button";
|
|
363
|
+
repoBtn.setAttribute("aria-haspopup", "dialog");
|
|
364
|
+
repoBtn.setAttribute("aria-controls", popoverId);
|
|
365
|
+
repoBtn.setAttribute("aria-expanded", String(popoverOpen));
|
|
366
|
+
icon(repoBtn, "branch");
|
|
367
|
+
repoBtn.appendChild(el("span", "och-repo-name", model.repo.name));
|
|
368
|
+
const fp = shortFingerprint(model.repo.fingerprint);
|
|
369
|
+
if (fp) repoBtn.appendChild(el("span", "och-repo-fp", fp));
|
|
370
|
+
icon(repoBtn, "chevron");
|
|
371
|
+
global.appendChild(repoBtn);
|
|
372
|
+
|
|
373
|
+
const pop = el("div", "och-pop");
|
|
374
|
+
pop.id = popoverId;
|
|
375
|
+
pop.hidden = !popoverOpen;
|
|
376
|
+
pop.setAttribute("role", "dialog");
|
|
377
|
+
pop.setAttribute("aria-label", "仓库身份");
|
|
378
|
+
pop.append(el("p", "och-pop-title", "仓库身份"));
|
|
379
|
+
let inspectState = "—";
|
|
380
|
+
if (model.repo.inspectReady !== undefined) {
|
|
381
|
+
inspectState = model.repo.inspectReady ? "ready" : "degraded";
|
|
382
|
+
}
|
|
383
|
+
const rows = [
|
|
384
|
+
["repo", model.repo.name],
|
|
385
|
+
["fingerprint", model.repo.fingerprint || "—"],
|
|
386
|
+
["package", model.repo.version || "—"],
|
|
387
|
+
["surface", "operate + inspect · 同进程"],
|
|
388
|
+
["inspect", inspectState],
|
|
389
|
+
];
|
|
390
|
+
for (const [k, v] of rows) {
|
|
391
|
+
const kv = el("div", "och-pop-kv");
|
|
392
|
+
kv.append(el("b", null, k), el("span", null, v));
|
|
393
|
+
pop.appendChild(kv);
|
|
394
|
+
}
|
|
395
|
+
global.appendChild(pop);
|
|
396
|
+
repoBtn.addEventListener("click", (event) => {
|
|
397
|
+
event.stopPropagation();
|
|
398
|
+
popoverOpen = !popoverOpen;
|
|
399
|
+
repoBtn.setAttribute("aria-expanded", String(popoverOpen));
|
|
400
|
+
pop.hidden = !popoverOpen;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const modeNav = el("nav", "och-mode");
|
|
405
|
+
modeNav.setAttribute("aria-label", "产品模式");
|
|
406
|
+
for (const btn of model.modeButtons) {
|
|
407
|
+
const a = el("a", `och-mode-btn${btn.active ? " och-on" : ""}`);
|
|
408
|
+
a.href = btn.href;
|
|
409
|
+
a.dataset.mode = btn.id;
|
|
410
|
+
if (btn.active) a.setAttribute("aria-current", "page");
|
|
411
|
+
a.appendChild(makeIcon(btn.id === "operate" ? "zap" : "eye"));
|
|
412
|
+
a.appendChild(document.createTextNode(btn.label));
|
|
413
|
+
modeNav.appendChild(a);
|
|
414
|
+
}
|
|
415
|
+
global.appendChild(modeNav);
|
|
416
|
+
|
|
417
|
+
if (model.context) {
|
|
418
|
+
const chip = el("div", "och-context");
|
|
419
|
+
chip.appendChild(el("span", "och-context-kind", model.context.kindLabel));
|
|
420
|
+
chip.appendChild(el("span", "och-context-id", model.context.id));
|
|
421
|
+
const back = el("a", "och-context-return");
|
|
422
|
+
back.href = model.context.returnHref;
|
|
423
|
+
back.appendChild(makeIcon("arrowLeft"));
|
|
424
|
+
back.appendChild(document.createTextNode(model.context.returnLabel));
|
|
425
|
+
chip.appendChild(back);
|
|
426
|
+
const close = el("button", "och-context-close");
|
|
427
|
+
close.type = "button";
|
|
428
|
+
close.setAttribute("aria-label", "清除选中上下文");
|
|
429
|
+
icon(close, "close");
|
|
430
|
+
close.addEventListener("click", () => {
|
|
431
|
+
currentConfig = { ...currentConfig, context: null };
|
|
432
|
+
if (currentConfig.liveLocation) {
|
|
433
|
+
history.replaceState(
|
|
434
|
+
null,
|
|
435
|
+
"",
|
|
436
|
+
clearInspectContextSource(window.location.hash),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
render();
|
|
440
|
+
});
|
|
441
|
+
chip.appendChild(close);
|
|
442
|
+
global.appendChild(chip);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
global.appendChild(el("span", "och-spacer"));
|
|
446
|
+
|
|
447
|
+
if (model.freshness) {
|
|
448
|
+
const fresh = el("span", "och-fresh");
|
|
449
|
+
fresh.setAttribute("aria-label", "数据新鲜度");
|
|
450
|
+
fresh.appendChild(el("span", "och-fresh-dot"));
|
|
451
|
+
const text = el("span", "och-fresh-text", freshnessText);
|
|
452
|
+
/* id 保留给 shell-chrome.updateHeaderRefresh 回写。 */
|
|
453
|
+
text.id = "header-refresh";
|
|
454
|
+
text.setAttribute("aria-live", "polite");
|
|
455
|
+
fresh.appendChild(text);
|
|
456
|
+
global.appendChild(fresh);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const pi = el("a", "och-pi");
|
|
460
|
+
pi.dataset.state = model.pi.state;
|
|
461
|
+
pi.href = model.pi.href;
|
|
462
|
+
if (model.pi.title) pi.title = model.pi.title;
|
|
463
|
+
pi.setAttribute("aria-label", `Pi 就绪:${model.pi.label}`);
|
|
464
|
+
pi.appendChild(el("span", "och-pi-label", "Pi"));
|
|
465
|
+
const piState = document.createElement("strong");
|
|
466
|
+
piState.textContent = model.pi.label;
|
|
467
|
+
pi.appendChild(piState);
|
|
468
|
+
global.appendChild(pi);
|
|
469
|
+
|
|
470
|
+
/* Band 2 */
|
|
471
|
+
const workspace = el("div", "och-workspace");
|
|
472
|
+
inner.appendChild(workspace);
|
|
473
|
+
const tabs = el("nav", "och-tabs");
|
|
474
|
+
tabs.setAttribute("aria-label", model.tabsAriaLabel);
|
|
475
|
+
for (const tab of model.tabs) {
|
|
476
|
+
const a = el(
|
|
477
|
+
"a",
|
|
478
|
+
`och-tab${tab.active ? " och-tab-active" : ""}`,
|
|
479
|
+
tab.label,
|
|
480
|
+
);
|
|
481
|
+
a.href = tab.href;
|
|
482
|
+
if (tab.active) a.setAttribute("aria-current", "page");
|
|
483
|
+
tabs.appendChild(a);
|
|
484
|
+
}
|
|
485
|
+
workspace.appendChild(tabs);
|
|
486
|
+
|
|
487
|
+
root.appendChild(chrome);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function render() {
|
|
491
|
+
build(currentModel());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function closePopover({ restoreFocus = false } = {}) {
|
|
495
|
+
if (!popoverOpen) return;
|
|
496
|
+
popoverOpen = false;
|
|
497
|
+
const repoBtn = root.querySelector(".och-repo");
|
|
498
|
+
const pop = root.querySelector(".och-pop");
|
|
499
|
+
repoBtn?.setAttribute("aria-expanded", "false");
|
|
500
|
+
if (pop) pop.hidden = true;
|
|
501
|
+
if (restoreFocus && repoBtn instanceof HTMLElement) repoBtn.focus();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function onHashChange() {
|
|
505
|
+
if (destroyed || !currentConfig.liveLocation) return;
|
|
506
|
+
popoverOpen = false;
|
|
507
|
+
render();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function onDocumentClick(event) {
|
|
511
|
+
if (!popoverOpen || !(event.target instanceof Node)) return;
|
|
512
|
+
const repoBtn = root.querySelector(".och-repo");
|
|
513
|
+
const pop = root.querySelector(".och-pop");
|
|
514
|
+
if (repoBtn?.contains(event.target) || pop?.contains(event.target)) return;
|
|
515
|
+
closePopover();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function onDocumentKeydown(event) {
|
|
519
|
+
if (event.key === "Escape") closePopover({ restoreFocus: true });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
window.addEventListener("hashchange", onHashChange);
|
|
523
|
+
document.addEventListener("click", onDocumentClick);
|
|
524
|
+
document.addEventListener("keydown", onDocumentKeydown);
|
|
525
|
+
|
|
526
|
+
render();
|
|
527
|
+
|
|
528
|
+
if (currentConfig.healthUrl && typeof fetch === "function") {
|
|
529
|
+
fetch(currentConfig.healthUrl, { credentials: "same-origin" })
|
|
530
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
531
|
+
.then((body) => {
|
|
532
|
+
if (destroyed || !body) return;
|
|
533
|
+
healthState = chromeStateFromHealth(body);
|
|
534
|
+
render();
|
|
535
|
+
})
|
|
536
|
+
.catch(() => {});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
update(nextConfig = {}) {
|
|
541
|
+
if (destroyed) return;
|
|
542
|
+
currentConfig = { ...currentConfig, ...nextConfig };
|
|
543
|
+
render();
|
|
544
|
+
},
|
|
545
|
+
destroy() {
|
|
546
|
+
destroyed = true;
|
|
547
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
548
|
+
document.removeEventListener("click", onDocumentClick);
|
|
549
|
+
document.removeEventListener("keydown", onDocumentKeydown);
|
|
550
|
+
root.textContent = "";
|
|
551
|
+
root.classList.remove("och-host");
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
@@ -12,17 +12,7 @@ export function showView(name) {
|
|
|
12
12
|
document.querySelectorAll("[data-view]").forEach((section) => {
|
|
13
13
|
section.hidden = section.dataset.view !== name;
|
|
14
14
|
});
|
|
15
|
-
|
|
16
|
-
if (name === "failures") activeHref = "#/failures";
|
|
17
|
-
else if (name === "pool" || name === "task") activeHref = "#/pool";
|
|
18
|
-
else if (name === "night") activeHref = "#/night";
|
|
19
|
-
else if (name === "feature") activeHref = "#/";
|
|
20
|
-
document.querySelectorAll(".nav-link").forEach((link) => {
|
|
21
|
-
link.classList.toggle(
|
|
22
|
-
"nav-link-active",
|
|
23
|
-
link.getAttribute("href") === activeHref,
|
|
24
|
-
);
|
|
25
|
-
});
|
|
15
|
+
// 工作区 tab 高亮由统一 Operator Chrome 依据 location.hash 自行维护。
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
export function setBreadcrumb(parts) {
|