@aiti-ai/aiti-claude-code-agent 0.0.0-stage → 0.1.8
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/AGENTS.md +32 -0
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/claude-handle.test.mjs +236 -0
- package/claude-quota.test.mjs +88 -0
- package/claude-quota.ts +147 -0
- package/client/src/controlled-login-client.mjs +22 -0
- package/client/src/controlled-login-dialog.mjs +270 -0
- package/dev-pack.mjs +246 -0
- package/dist/controlled-login-client.mjs +22 -0
- package/dist/controlled-login-dialog.mjs +270 -0
- package/external-governance.ts +138 -0
- package/index.ts +1431 -0
- package/package.json +28 -4
- package/reconcile-deps.mjs +41 -0
- package/reload-instances.mjs +60 -0
- package/tools/deploy.mjs +45 -0
- package/tools/turn-ack-probe.mjs +106 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* controlled-login — 外部 Agent 受控登录标准 · 前端统一弹窗(原生 h() 渲染,零构建步骤)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠ 副本同步(字节级拷贝,改动须同步到全部副本;测试 `_shared/controlled-login.test.mjs` 钉住一致性)。
|
|
5
|
+
* 权威源:pi-web-ext/_shared/controlled-login-dialog.mjs
|
|
6
|
+
* 部署副本(client 侧走部署树内相对导入 —— 零构建 client 无法像 vite 客户端那样在构建期
|
|
7
|
+
* 内联 ../../_shared(资产路由拒绝越目录穿越),与 draft-keeper 的 ./core.mjs 同一惯例):
|
|
8
|
+
* pi-web-ext/aiti-{zcode,cursor,qoder,claude-code,codex}-agent/client/controlled-login-dialog.mjs
|
|
9
|
+
*
|
|
10
|
+
* 契约(docs/ext-controlled-login-standard.md §3.5):
|
|
11
|
+
* 🔗「打开授权页」按钮(target=_blank)+ URL 文本可复制 + 模式说明行
|
|
12
|
+
* + 状态区(waiting 转圈 / success ✅ 用户名 / failed / timeout / cancelled,均带重试)
|
|
13
|
+
* + B 模式(paste)专属 code 输入框+提交。
|
|
14
|
+
* 状态刷新:弹窗打开期间每 2s 轮询 GET /login/status,关闭即停
|
|
15
|
+
* (方案 §3.6 已论证并拍板的轮询例外:登录是分钟级一次性交互)。
|
|
16
|
+
*
|
|
17
|
+
* 浮动契约:面板根节点 data-ext-float="1" + data-ext-float-w/-h + data-ext-float-full="1"
|
|
18
|
+
* (aiti-float 接管拖拽/缩放/持久化/小屏全屏;未装时自身 style 居中兜底)。
|
|
19
|
+
*
|
|
20
|
+
* 接入(各扩展 client/controlled-login-client.mjs,两行):
|
|
21
|
+
* import { registerControlledLoginClient } from "./controlled-login-dialog.mjs";
|
|
22
|
+
* export default function register(ctx) { registerControlledLoginClient(ctx, { … }); }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ---------- 模块内 open 状态(toolbar 入口与 dialogs 条目同模块共享,不落 window 全局) ----------
|
|
26
|
+
let moduleSetOpen = null;
|
|
27
|
+
// 懒加载(clientImmediately:false)下 toolbar 静态条目点击时弹窗组件可能尚未挂载:
|
|
28
|
+
// 记 pending,挂载 effect 消费——避免首点失效。
|
|
29
|
+
let pendingOpen = false;
|
|
30
|
+
|
|
31
|
+
/** 打开本扩展的受控登录弹窗(toolbar 🔑 按钮 / 静态条目 meta.onClick 用)。 */
|
|
32
|
+
export function openControlledLogin() {
|
|
33
|
+
if (moduleSetOpen) moduleSetOpen(true);
|
|
34
|
+
else pendingOpen = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------- 自持样式层(幂等注入;clg- 前缀 + var(--x, 兜底),零宿主耦合) ----------
|
|
38
|
+
const STYLE_ID = "aiti-controlled-login-styles";
|
|
39
|
+
const STYLES = `
|
|
40
|
+
.clg-mask { position: fixed; inset: 0; z-index: 2147483000; background: rgba(0,0,0,.32); display: flex; align-items: center; justify-content: center; }
|
|
41
|
+
.clg-panel { width: 520px; max-width: calc(100vw - 16px); display: flex; flex-direction: column; overflow: hidden;
|
|
42
|
+
background: var(--bg,#fff); color: var(--text,#1a1a1a); border: 1px solid var(--border,#e0e0e0); border-radius: 12px;
|
|
43
|
+
box-shadow: 0 16px 64px rgba(0,0,0,.25); font-size: 13px; line-height: 1.55; }
|
|
44
|
+
.clg-head { display: flex; align-items: center; gap: 10px; padding: 11px 16px; border-bottom: 1px solid var(--border,#e0e0e0); flex-shrink: 0; background: var(--bg-subtle,#f7f7f7); }
|
|
45
|
+
.clg-title { font-size: 14px; font-weight: 700; display: flex; align-items: center; gap: 6px; }
|
|
46
|
+
.clg-x { margin-left: auto; border: none; background: none; cursor: pointer; color: var(--text-muted,#888); font-size: 16px; line-height: 1; padding: 4px 6px; border-radius: 6px; }
|
|
47
|
+
.clg-x:hover { background: var(--bg-hover,#eee); color: var(--text,#1a1a1a); }
|
|
48
|
+
.clg-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
49
|
+
.clg-intro { color: var(--text-muted,#777); font-size: 12.5px; }
|
|
50
|
+
.clg-urlrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
51
|
+
.clg-open { display: inline-flex; align-items: center; gap: 6px; padding: 7px 16px; border: 1px solid var(--accent,#2563eb); border-radius: 8px;
|
|
52
|
+
cursor: pointer; background: var(--accent,#2563eb); color: #fff; font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; }
|
|
53
|
+
.clg-open:hover { background: var(--accent-hover,#1d4ed8); }
|
|
54
|
+
.clg-url { flex: 1; min-width: 200px; font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-muted,#777);
|
|
55
|
+
word-break: break-all; cursor: pointer; padding: 5px 8px; border: 1px dashed var(--border,#d8d8d8); border-radius: 6px; }
|
|
56
|
+
.clg-url:hover { border-color: var(--accent,#2563eb); color: var(--text,#1a1a1a); }
|
|
57
|
+
.clg-copied { color: #16a34a; font-size: 11.5px; }
|
|
58
|
+
.clg-status { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-radius: 8px; font-size: 12.5px; flex-wrap: wrap; }
|
|
59
|
+
.clg-status.waiting { background: rgba(37,99,235,.08); color: var(--accent,#2563eb); }
|
|
60
|
+
.clg-status.success { background: color-mix(in srgb, var(--ok, #22c55e) 12%, transparent); color: #16a34a; }
|
|
61
|
+
.clg-status.failed, .clg-status.timeout, .clg-status.cancelled { background: color-mix(in srgb, var(--danger, #ef4444) 8%, transparent); color: #dc2626; }
|
|
62
|
+
.clg-spin { width: 14px; height: 14px; flex-shrink: 0; border: 2px solid color-mix(in srgb, currentColor 25%, transparent); border-top-color: currentColor;
|
|
63
|
+
border-radius: 50%; animation: clg-rotate .9s linear infinite; }
|
|
64
|
+
@keyframes clg-rotate { to { transform: rotate(360deg); } }
|
|
65
|
+
.clg-errline { font-size: 12px; color: #dc2626; word-break: break-all; }
|
|
66
|
+
.clg-tail { font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-muted,#888); background: var(--bg-subtle,#f5f5f5);
|
|
67
|
+
border: 1px solid var(--border,#e0e0e0); border-radius: 8px; padding: 8px 10px; max-height: 140px; overflow: auto; white-space: pre-wrap; word-break: break-all; }
|
|
68
|
+
.clg-coderow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
69
|
+
.clg-input { flex: 1; min-width: 180px; padding: 7px 10px; border: 1px solid var(--border,#d8d8d8); border-radius: 8px;
|
|
70
|
+
background: var(--bg,#fff); color: var(--text,#1a1a1a); font-size: 12.5px; outline: none; }
|
|
71
|
+
.clg-input:focus { border-color: var(--accent,#2563eb); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent,#2563eb) 18%, transparent); }
|
|
72
|
+
.clg-btn { display: inline-flex; align-items: center; gap: 5px; padding: 6px 14px; border: 1px solid var(--border,#d8d8d8); border-radius: 7px;
|
|
73
|
+
cursor: pointer; background: var(--bg,#fff); color: var(--text,#1a1a1a); font-size: 12.5px; font-weight: 500; white-space: nowrap; }
|
|
74
|
+
.clg-btn:hover { background: var(--bg-hover,#f0f0f0); }
|
|
75
|
+
.clg-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
76
|
+
.clg-btn.primary { background: var(--accent,#2563eb); border-color: var(--accent,#2563eb); color: #fff; }
|
|
77
|
+
.clg-btn.primary:hover { background: var(--accent-hover,#1d4ed8); }
|
|
78
|
+
.clg-foot { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
|
|
79
|
+
.clg-hint { color: var(--text-dim,#999); font-size: 11.5px; }
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
let stylesInjected = false;
|
|
83
|
+
function ensureStyles() {
|
|
84
|
+
if (stylesInjected) return;
|
|
85
|
+
if (typeof document === "undefined") return;
|
|
86
|
+
if (!document.getElementById(STYLE_ID)) {
|
|
87
|
+
const el = document.createElement("style");
|
|
88
|
+
el.id = STYLE_ID;
|
|
89
|
+
el.textContent = STYLES;
|
|
90
|
+
document.head.appendChild(el);
|
|
91
|
+
}
|
|
92
|
+
stylesInjected = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 统一弹窗注册(每扩展 client 的薄壳调用一次)。
|
|
97
|
+
*
|
|
98
|
+
* @param {*} ctx 宿主 client 上下文(h/useState/useEffect/apiUrl/registerItems)
|
|
99
|
+
* @param {object} config
|
|
100
|
+
* @param {string} config.extId 扩展 id(API 前缀 / toolbar item id 用)
|
|
101
|
+
* @param {string} config.toolbarLabel 工具条按钮 title/label(如 "登录 ZCode")
|
|
102
|
+
* @param {string} config.title 弹窗标题
|
|
103
|
+
* @param {string} [config.urlHint] A url-only 模式说明行
|
|
104
|
+
* @param {string} [config.pasteHint] B paste-relay 模式说明行
|
|
105
|
+
* @param {string} [config.successHint] 成功后的补充说明(如凭据换 key 提示)
|
|
106
|
+
*/
|
|
107
|
+
export function registerControlledLoginClient(ctx, config) {
|
|
108
|
+
ensureStyles();
|
|
109
|
+
const h = (...a) => ctx.h(...a);
|
|
110
|
+
const useState = (v) => ctx.useState(v);
|
|
111
|
+
const useEffect = (fn, deps) => ctx.useEffect(fn, deps);
|
|
112
|
+
const api = (p, init) => fetch(ctx.apiUrl("/api/extensions/" + config.extId + p), { cache: "no-store", ...init });
|
|
113
|
+
const jsonInit = (body) => ({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
114
|
+
|
|
115
|
+
// ---- 工具条入口:声明式图标按钮(meta.icon + meta.onClick、无 render —— 宿主统一渲染 36×36 线图按钮,禁 emoji) ----
|
|
116
|
+
ctx.registerItems("toolbar", [{
|
|
117
|
+
id: `${config.extId}-controlled-login`,
|
|
118
|
+
meta: {
|
|
119
|
+
label: config.toolbarLabel,
|
|
120
|
+
title: config.toolbarLabel,
|
|
121
|
+
icon: "icons/key-round.svg",
|
|
122
|
+
onClick: () => openControlledLogin(),
|
|
123
|
+
},
|
|
124
|
+
}]);
|
|
125
|
+
// ---- 弹窗条目(dialogs slot;宿主包 display:contents 壳,float 持久化键认它) ----
|
|
126
|
+
ctx.registerItems("dialogs", [{
|
|
127
|
+
id: `${config.extId}-controlled-login-dialog`,
|
|
128
|
+
render: () => h(ControlledLoginDialog),
|
|
129
|
+
}]);
|
|
130
|
+
|
|
131
|
+
function ControlledLoginDialog() {
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const [status, setStatus] = useState(null); // /login/status 响应({state,...})
|
|
134
|
+
const [busy, setBusy] = useState(false);
|
|
135
|
+
const [reqError, setReqError] = useState(null); // 请求层错误(网络/400)
|
|
136
|
+
const [code, setCode] = useState("");
|
|
137
|
+
const [copied, setCopied] = useState(false);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
moduleSetOpen = setOpen;
|
|
141
|
+
if (pendingOpen) { pendingOpen = false; setOpen(true); }
|
|
142
|
+
return () => { if (moduleSetOpen === setOpen) moduleSetOpen = null; };
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
// 打开期间 2s 轮询 /login/status(§3.6 拍板例外);关闭即停
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!open) return;
|
|
148
|
+
let stopped = false;
|
|
149
|
+
let timer = null;
|
|
150
|
+
const tick = async () => {
|
|
151
|
+
try {
|
|
152
|
+
const r = await api("/login/status");
|
|
153
|
+
const d = await r.json();
|
|
154
|
+
if (!stopped) { setStatus(d); setReqError(null); }
|
|
155
|
+
} catch { /* 网络抖动忽略,下轮再试 */ }
|
|
156
|
+
if (!stopped) timer = setTimeout(tick, 2000);
|
|
157
|
+
};
|
|
158
|
+
tick();
|
|
159
|
+
return () => { stopped = true; if (timer) clearTimeout(timer); };
|
|
160
|
+
}, [open]);
|
|
161
|
+
|
|
162
|
+
if (!open) return null;
|
|
163
|
+
|
|
164
|
+
const st = status?.state ?? "idle";
|
|
165
|
+
const mode = status?.mode ?? "url";
|
|
166
|
+
const pasteMode = mode === "paste";
|
|
167
|
+
|
|
168
|
+
async function call(p, init) {
|
|
169
|
+
setBusy(true); setReqError(null);
|
|
170
|
+
try {
|
|
171
|
+
const r = await api(p, init);
|
|
172
|
+
const d = await r.json();
|
|
173
|
+
if (d && d.error) setReqError(d.error);
|
|
174
|
+
if (d) setStatus(d);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
setReqError(e?.message || String(e));
|
|
177
|
+
}
|
|
178
|
+
setBusy(false);
|
|
179
|
+
}
|
|
180
|
+
const startLogin = () => call("/login/start", jsonInit({}));
|
|
181
|
+
const cancelLogin = () => call("/login/cancel", jsonInit({}));
|
|
182
|
+
const submitCode = () => { if (code.trim()) call("/login/submit", jsonInit({ code })); };
|
|
183
|
+
|
|
184
|
+
async function copyUrl() {
|
|
185
|
+
const url = status?.url || "";
|
|
186
|
+
if (!url) return;
|
|
187
|
+
try { await navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { /* 剪贴板不可用:URL 文本本身可选中复制 */ }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- 状态区 ----
|
|
191
|
+
let statusLine = null;
|
|
192
|
+
if (st === "idle") {
|
|
193
|
+
statusLine = h("div", { className: "clg-status waiting" }, "尚未发起登录");
|
|
194
|
+
} else if (st === "waiting") {
|
|
195
|
+
statusLine = h("div", { className: "clg-status waiting" },
|
|
196
|
+
h("span", { className: "clg-spin" }),
|
|
197
|
+
status?.url ? "等待你在浏览器完成授权…(本页自动感知,约 2s 内)" : "正在启动官方登录进程,等待授权链接…",
|
|
198
|
+
h("button", { className: "clg-btn", disabled: busy, style: { marginLeft: "auto" }, onClick: cancelLogin }, "取消"),
|
|
199
|
+
);
|
|
200
|
+
} else if (st === "success") {
|
|
201
|
+
statusLine = h("div", { className: "clg-status success" },
|
|
202
|
+
"登录成功", status?.user ? h("span", { style: { fontWeight: 700 } }, status.user) : null,
|
|
203
|
+
config.successHint ? h("span", { className: "clg-hint", style: { color: "inherit" } }, config.successHint) : null,
|
|
204
|
+
);
|
|
205
|
+
} else if (st === "failed" || st === "timeout" || st === "cancelled") {
|
|
206
|
+
const label = st === "failed" ? "登录失败" : st === "timeout" ? "登录超时" : "已取消";
|
|
207
|
+
statusLine = h("div", { className: "clg-status " + st },
|
|
208
|
+
h("span", null, (st === "timeout" ? "⏱ " : st === "cancelled" ? "⚪ " : "❌ ") + label),
|
|
209
|
+
status?.error ? h("span", { className: "clg-errline" }, status.error) : null,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return h("div", { className: "clg-mask", onClick: () => setOpen(false) },
|
|
214
|
+
h("div", {
|
|
215
|
+
className: "clg-panel",
|
|
216
|
+
"data-ext-float": "1", "data-ext-float-w": 520, "data-ext-float-h": 380, "data-ext-float-full": "1",
|
|
217
|
+
onClick: (e) => e.stopPropagation(),
|
|
218
|
+
},
|
|
219
|
+
// ── 标题栏(首个子元素 = float 拖拽把手)──
|
|
220
|
+
h("div", { className: "clg-head" },
|
|
221
|
+
h("span", { className: "clg-title" }, "🔑 " + config.title),
|
|
222
|
+
h("button", { className: "clg-x", title: "关闭", onClick: () => setOpen(false) }, "✕"),
|
|
223
|
+
),
|
|
224
|
+
h("div", { className: "clg-body" },
|
|
225
|
+
config.urlHint && !pasteMode ? h("div", { className: "clg-intro" }, config.urlHint) : null,
|
|
226
|
+
pasteMode && config.pasteHint ? h("div", { className: "clg-intro" }, config.pasteHint) : null,
|
|
227
|
+
|
|
228
|
+
// ── URL 区(waiting 且已抓到链接)──
|
|
229
|
+
st === "waiting" && status?.url
|
|
230
|
+
? h("div", { className: "clg-urlrow" },
|
|
231
|
+
h("a", { className: "clg-open", href: status.url, target: "_blank", rel: "noopener noreferrer" }, "打开授权页"),
|
|
232
|
+
h("span", { className: "clg-url", title: "点击复制链接", onClick: copyUrl }, status.url),
|
|
233
|
+
copied ? h("span", { className: "clg-copied" }, "已复制") : null,
|
|
234
|
+
)
|
|
235
|
+
: null,
|
|
236
|
+
|
|
237
|
+
// ── B 模式 code 输入(paste 且 waiting)──
|
|
238
|
+
pasteMode && st === "waiting"
|
|
239
|
+
? h("div", { className: "clg-coderow" },
|
|
240
|
+
h("input", {
|
|
241
|
+
className: "clg-input",
|
|
242
|
+
placeholder: "粘贴授权完成页显示的 code(或完整回调链接)",
|
|
243
|
+
value: code,
|
|
244
|
+
onChange: (e) => setCode(e.target.value),
|
|
245
|
+
onKeyDown: (e) => { if (e.key === "Enter") submitCode(); },
|
|
246
|
+
}),
|
|
247
|
+
h("button", { className: "clg-btn primary", disabled: busy || !code.trim(), onClick: submitCode }, busy ? "…" : "提交 code"),
|
|
248
|
+
)
|
|
249
|
+
: null,
|
|
250
|
+
|
|
251
|
+
statusLine,
|
|
252
|
+
reqError ? h("div", { className: "clg-errline" }, reqError) : null,
|
|
253
|
+
|
|
254
|
+
// CLI 升级改输出格式导致 URL 抓取失败时:原文透出(人工可读可复制,方案 §7)
|
|
255
|
+
(st === "failed" || st === "timeout") && status?.outputTail
|
|
256
|
+
? h("div", { className: "clg-tail" }, status.outputTail)
|
|
257
|
+
: null,
|
|
258
|
+
|
|
259
|
+
// ── 底部动作:idle/终态带(重)试 ──
|
|
260
|
+
h("div", { className: "clg-foot" },
|
|
261
|
+
st === "idle" || st === "failed" || st === "timeout" || st === "cancelled" || st === "success"
|
|
262
|
+
? h("button", { className: "clg-btn primary", disabled: busy, onClick: startLogin }, busy ? "…" : st === "idle" ? "发起登录" : "重试")
|
|
263
|
+
: null,
|
|
264
|
+
h("span", { className: "clg-hint" }, "授权在浏览器完成,凭据由官方 CLI 落盘;本页关闭不影响进行中的登录。"),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
package/dev-pack.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-web-ext · dev-pack 共享实现(方案 §3.2 / M5:扩展 dev-pack——src + 构建工具 + AGENTS.md 直达部署目录)
|
|
3
|
+
*
|
|
4
|
+
* 目标:用户在 AI 窗对 AI 说「帮我改一下 xxx 扩展」时,AI 能在**部署目录**直接:
|
|
5
|
+
* 读 AGENTS.md → 改 src/ → node tools/build.mjs → curl reload → 浏览器生效,不等回开发机。
|
|
6
|
+
*
|
|
7
|
+
* 各扩展 tools/deploy.mjs 末尾统一追加一段(见本文件同级各 deploy.mjs 模式):
|
|
8
|
+
* const { packDevPack } = await import("../../_shared/dev-pack.mjs");
|
|
9
|
+
* packDevPack({ extRoot: <扩展源码根>, log: console.log });
|
|
10
|
+
*
|
|
11
|
+
* packDevPack 向每个已存在的部署目录(~/.pi/web{,-dev}/extensions/<extId>)写入:
|
|
12
|
+
* AGENTS.md 源码目录有则拷,无则由模板生成(§3.4)
|
|
13
|
+
* src/ client/src/**、根级服务端源码 *.ts/*.cjs/*.mjs、lib/(跳 vendor)、server/、sched/、worker-src/
|
|
14
|
+
* tools/build.mjs 现场构建入口(扩展自带则拷真身,否则生成 esbuild 默认版)
|
|
15
|
+
* tools/deploy.mjs 部署脚本本身(大改后回源码仓重部署的入口)
|
|
16
|
+
* package.json 含 aitiConfigs 声明与 build 脚本
|
|
17
|
+
* vite.config.ts 有则拷
|
|
18
|
+
*
|
|
19
|
+
* 幂等:重复部署整目录覆盖;允许失败:任何异常只 log 不影响 deploy 退出码(调用方 try/catch)。
|
|
20
|
+
*/
|
|
21
|
+
"use strict";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join, resolve, dirname, basename, sep as pathSep } from "node:path";
|
|
24
|
+
import {
|
|
25
|
+
existsSync, readdirSync, statSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
|
|
28
|
+
// 根级服务端源码文件:拷进 src/(运行时平铺文件在部署根不动,宿主 loader 路径不变)
|
|
29
|
+
const ROOT_SRC_EXTS = new Set([".ts", ".tsx", ".cjs", ".mjs"]);
|
|
30
|
+
const ROOT_SRC_SKIP = new Set(["dist", "node_modules"]);
|
|
31
|
+
// 源码子目录:整目录拷(vendor 类大资产跳过——部署目录本就有运行时副本)
|
|
32
|
+
const SRC_DIRS = ["client/src", "server", "lib", "sched", "worker-src"];
|
|
33
|
+
const DIR_SKIP = new Set(["vendor", "__pycache__", "node_modules"]);
|
|
34
|
+
|
|
35
|
+
function copyTree(src, dest, skip) {
|
|
36
|
+
mkdirSync(dest, { recursive: true });
|
|
37
|
+
for (const en of readdirSync(src, { withFileTypes: true })) {
|
|
38
|
+
if (skip && skip.has(en.name)) continue;
|
|
39
|
+
const s = join(src, en.name), d = join(dest, en.name);
|
|
40
|
+
if (en.isDirectory()) copyTree(s, d, skip);
|
|
41
|
+
else if (en.isFile()) cpSync(s, d);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** AGENTS.md 默认模板(§3.4;扩展自带 AGENTS.md 则不走到这里)。供 CLI 批量生成源码目录版复用。 */
|
|
46
|
+
export function agentsMdTemplate(extId) {
|
|
47
|
+
return `# ${extId} 扩展(部署目录直改指南)
|
|
48
|
+
|
|
49
|
+
## 结构
|
|
50
|
+
- 运行时文件平铺在本目录根(index.ts / core.cjs …,宿主 loader 按相对路径加载——**禁手改运行时平铺文件**,改 src/ 再 build)
|
|
51
|
+
- \`src/\` 源码(client/src + 服务端 ts/cjs)· \`dist/\` 构建产物(禁手改)· \`tools/\` 构建/部署脚本
|
|
52
|
+
- npm 包形态的扩展:本目录 package.json 含 version 与 aitiConfigs 声明
|
|
53
|
+
|
|
54
|
+
## 小改现场流程(改文案/样式/小逻辑)
|
|
55
|
+
1. 改 \`src/\` 下对应源码
|
|
56
|
+
2. \`node tools/build.mjs\`(client 有构建产物的扩展;纯服务端改动跳过)
|
|
57
|
+
3. \`curl -X POST http://127.0.0.1:3001/api/extensions/reload\`(dev;prod 3002;外部经 nginx 带 basePath:/dev、/mac、/mdev、/win、/wdev → \`/xxx/api/extensions/reload\`)
|
|
58
|
+
4. 浏览器刷新页面验证
|
|
59
|
+
|
|
60
|
+
## 大改回源码仓
|
|
61
|
+
- 源码仓:\`extensions/${extId}/\`(git 管理,deploy 同步回本目录)
|
|
62
|
+
- 改完在源码仓跑 \`node tools/deploy.mjs\`(或仓库根 \`node scripts/deploy-all.mjs --only ${extId}\`)
|
|
63
|
+
|
|
64
|
+
## 数据目录(数据分层:config / cache)
|
|
65
|
+
- \`ctx.configDir\` 根级 = 配置与关键小状态(aiti-configs 备份范围;小、珍贵、人可读)
|
|
66
|
+
- \`ctx.cacheDir\`(= \`<configDir>/cache/\`)= 可删缓存:db/venv/日志/大文件;rm -rf 后扩展下次访问自动重建
|
|
67
|
+
- \`ctx.dataDir\` 已移除(访问即抛)——新代码禁用
|
|
68
|
+
- 本扩展白名单(永不为 cache 的配置文件)见 package.json \`aitiConfigs.configFiles\`
|
|
69
|
+
|
|
70
|
+
## 红线
|
|
71
|
+
- 禁轮询(setInterval 拉取);需要等待用事件/回调/SSE(桥的 SSE、aiti-event-bus topics)
|
|
72
|
+
- 禁重启生产服务(pi-web 3002 等 restart 须用户明确同意)
|
|
73
|
+
- 禁手改 dist/ 与运行时平铺文件;跨扩展通信走 ctx.services(禁 globalThis.__ext 直穿)
|
|
74
|
+
- 服务端:SQLite 一律 WAL + 分批小事务(批间 setImmediate 让步);禁 execFileSync 调 >1s 外部程序;>8MB 文件禁同步读
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 默认 tools/build.mjs(esbuild 单包零配置;扩展自带 tools/build.mjs 则不走到这里)。 */
|
|
79
|
+
function buildMjsTemplate(extId, extRoot, clientOut, entryRel) {
|
|
80
|
+
const repoHint = "/home/pi/workspace/pi-work"; // 现场构建探测源码仓 node_modules 用(三机各不同,缺失时走 npm exec)
|
|
81
|
+
return `/**
|
|
82
|
+
* ${extId} · 现场构建入口(dev-pack 默认模板,esbuild 单包零配置)
|
|
83
|
+
* 用法:node tools/build.mjs
|
|
84
|
+
* 本文件由 dev-pack 生成;复杂构建(多入口/插件链)请参照 src/vite.config.ts 改写本文件或回源码仓构建。
|
|
85
|
+
*/
|
|
86
|
+
"use strict";
|
|
87
|
+
import { existsSync, readdirSync, constants as fsConstants, accessSync } from "node:fs";
|
|
88
|
+
import { join, resolve, dirname, basename, sep as pathSep } from "node:path";
|
|
89
|
+
import { fileURLToPath } from "node:url";
|
|
90
|
+
import { spawnSync } from "node:child_process";
|
|
91
|
+
|
|
92
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
93
|
+
const extRoot = resolve(here, "..");
|
|
94
|
+
const clientOut = ${JSON.stringify(clientOut || "dist/client-bundle.mjs")};
|
|
95
|
+
const entryRel = ${JSON.stringify(entryRel || "")};
|
|
96
|
+
|
|
97
|
+
// esbuild 解析:部署目录 node_modules → 源码仓 node_modules(本机开发场景)→ PATH 上的 esbuild → npm exec 现拉。
|
|
98
|
+
// 注意:node_modules/esbuild/bin/esbuild 是原生二进制(ELF/Mach-O),必须直接执行(不能当 JS 跑)。
|
|
99
|
+
function findEsbuild() {
|
|
100
|
+
const home = (process.env.HOME || process.env.USERPROFILE || "").split(pathSep).join("/");
|
|
101
|
+
const candidates = [
|
|
102
|
+
extRoot + "/node_modules/esbuild/bin/esbuild",
|
|
103
|
+
home + "/workspace/pi-work/node_modules/esbuild/bin/esbuild",
|
|
104
|
+
"D:/workspace/pi-work/node_modules/esbuild/bin/esbuild", // 155 win(源码在 D 盘)
|
|
105
|
+
];
|
|
106
|
+
for (const c of candidates) {
|
|
107
|
+
if (!existsSync(c)) continue;
|
|
108
|
+
try { accessSync(c, fsConstants.X_OK); return [c]; } catch (e) { /* 无执行位 → 下一个来源 */ }
|
|
109
|
+
}
|
|
110
|
+
const r = spawnSync("esbuild", ["--version"], { encoding: "utf8" });
|
|
111
|
+
if (r.status === 0) return ["esbuild"];
|
|
112
|
+
return ["npm", "exec", "--yes", "esbuild", "--"];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pickEntry() {
|
|
116
|
+
if (entryRel) return entryRel;
|
|
117
|
+
const srcDir = join(extRoot, "src", "client", "src");
|
|
118
|
+
if (!existsSync(srcDir)) throw new Error("找不到 client 源码目录 src/client/src");
|
|
119
|
+
for (const name of ["entry.tsx", "entry.ts", "entry.jsx", "entry.js", "index.tsx", "index.ts"]) {
|
|
120
|
+
if (existsSync(join(srcDir, name))) return "client/src/" + name;
|
|
121
|
+
}
|
|
122
|
+
const first = readdirSync(srcDir).find((f) => /\\.(tsx?|jsx?)$/.test(f));
|
|
123
|
+
if (!first) throw new Error("client/src 下没有可构建入口");
|
|
124
|
+
return "client/src/" + first;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 服务端源码同步:src/ 根级文件(index.ts/core.cjs/package.json/vite.config.ts 等)→ 部署根
|
|
128
|
+
// (服务端没有构建产物——jiti 直接加载部署根文件;不 sync 则「改 src → reload」永远不生效)
|
|
129
|
+
import { readdirSync as _rd, copyFileSync as _cp, statSync as _st } from "node:fs";
|
|
130
|
+
const srcRoot = join(extRoot, "src");
|
|
131
|
+
if (existsSync(srcRoot)) {
|
|
132
|
+
for (const f of _rd(srcRoot)) {
|
|
133
|
+
const fp = join(srcRoot, f);
|
|
134
|
+
if (!_st(fp).isFile()) continue;
|
|
135
|
+
_cp(fp, join(extRoot, f));
|
|
136
|
+
}
|
|
137
|
+
console.log("[build] 服务端源码已从 src/ 同步到部署根");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const entry = pickEntry();
|
|
141
|
+
const args = [join(extRoot, "src", entry), "--bundle", "--format=esm",
|
|
142
|
+
"--outfile=" + join(extRoot, clientOut), "--jsx=automatic"];
|
|
143
|
+
// pi-web host-shim 委托(react → 本地 shim;jsx-runtime 同理)——与各扩展 vite.config 同款
|
|
144
|
+
const shim = join(extRoot, "src", "client", "src", "host-shim.ts");
|
|
145
|
+
const jsxRuntime = ["jsx-runtime.ts", "jsx-runtime.tsx", "jsx-dev-runtime.ts"].map((f) => join(extRoot, "src", "client", "src", f)).find(existsSync);
|
|
146
|
+
if (existsSync(shim)) args.push("--alias:react=" + shim);
|
|
147
|
+
if (jsxRuntime) args.push("--alias:react/jsx-runtime=" + jsxRuntime);
|
|
148
|
+
args.push("--log-level=info");
|
|
149
|
+
|
|
150
|
+
const [cmd, ...pre] = findEsbuild();
|
|
151
|
+
console.log("[build] esbuild:", cmd, ...pre);
|
|
152
|
+
const r = spawnSync(cmd, [...pre, ...args], { stdio: "inherit", cwd: extRoot });
|
|
153
|
+
if (r.status !== 0) { console.error("[build] 失败——可参照 src/vite.config.ts 调整本文件后重试,或回源码仓构建"); process.exit(r.status ?? 1); }
|
|
154
|
+
console.log("[build] ✓ " + clientOut + "(reload 后浏览器刷新生效)");
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 主入口:向已存在的部署目录写 dev-pack。
|
|
160
|
+
* @param {object} opts
|
|
161
|
+
* @param {string} opts.extRoot 扩展源码根(pi-web-ext/<extId>)
|
|
162
|
+
* @param {string[]} [opts.targets] 显式部署目录;缺省 = ~/.pi/web{,-dev}/extensions/<extId>(存在的才写)
|
|
163
|
+
* @param {(m: string) => void} [opts.log]
|
|
164
|
+
*/
|
|
165
|
+
export function packDevPack(opts) {
|
|
166
|
+
const extRoot = resolve(opts.extRoot);
|
|
167
|
+
const extId = basename(extRoot);
|
|
168
|
+
const log = opts.log || (() => {});
|
|
169
|
+
const targets = (opts.targets || [
|
|
170
|
+
join(homedir(), ".pi", "web", "extensions", extId),
|
|
171
|
+
join(homedir(), ".pi", "web-dev", "extensions", extId),
|
|
172
|
+
]).filter((t) => existsSync(t));
|
|
173
|
+
if (!targets.length) return;
|
|
174
|
+
|
|
175
|
+
// 1) 打包源码到临时区(staging)→ 逐目标 cpSync(多部署目录零重复扫描)
|
|
176
|
+
const stage = join(extRoot, "dist", ".dev-pack-staging");
|
|
177
|
+
rmSync(stage, { recursive: true, force: true });
|
|
178
|
+
mkdirSync(stage, { recursive: true });
|
|
179
|
+
try {
|
|
180
|
+
// src/:根级源码文件 + 源码子目录
|
|
181
|
+
const srcDir = join(stage, "src");
|
|
182
|
+
mkdirSync(srcDir, { recursive: true });
|
|
183
|
+
for (const en of readdirSync(extRoot, { withFileTypes: true })) {
|
|
184
|
+
if (en.isDirectory()) continue;
|
|
185
|
+
if (ROOT_SRC_SKIP.has(en.name)) continue;
|
|
186
|
+
if (!ROOT_SRC_EXTS.has(en.name.slice(en.name.lastIndexOf(".") || 0)) && en.name !== "package.json") continue;
|
|
187
|
+
cpSync(join(extRoot, en.name), join(srcDir, en.name));
|
|
188
|
+
}
|
|
189
|
+
for (const d of SRC_DIRS) {
|
|
190
|
+
const s = join(extRoot, d);
|
|
191
|
+
if (!existsSync(s)) continue;
|
|
192
|
+
copyTree(s, join(srcDir, ...d.split("/")), DIR_SKIP);
|
|
193
|
+
}
|
|
194
|
+
for (const f of ["vite.config.ts", "package.json"]) {
|
|
195
|
+
if (existsSync(join(extRoot, f))) cpSync(join(extRoot, f), join(srcDir, f));
|
|
196
|
+
}
|
|
197
|
+
// tools/build.mjs:扩展自带拷真身,否则生成默认 esbuild 版
|
|
198
|
+
mkdirSync(join(stage, "tools"), { recursive: true });
|
|
199
|
+
if (existsSync(join(extRoot, "tools", "build.mjs"))) {
|
|
200
|
+
cpSync(join(extRoot, "tools", "build.mjs"), join(stage, "tools", "build.mjs"));
|
|
201
|
+
} else {
|
|
202
|
+
let clientOut = "";
|
|
203
|
+
try {
|
|
204
|
+
const pkg = JSON.parse(readFileSync(join(extRoot, "package.json"), "utf8"));
|
|
205
|
+
if (pkg.client && String(pkg.client).startsWith("dist/")) clientOut = pkg.client;
|
|
206
|
+
} catch (e) {}
|
|
207
|
+
if (!clientOut) {
|
|
208
|
+
// 回退:client 产物名常声明在 entry 的 client: "dist/<id>-client.mjs"(index.ts)——从 staging 的 src 里解析
|
|
209
|
+
try {
|
|
210
|
+
for (const f of readdirSync(srcDir)) {
|
|
211
|
+
if (!/\.tsx?$/.test(f)) continue;
|
|
212
|
+
const m = readFileSync(join(srcDir, f), "utf8").match(/client\s*:\s*["'`](dist\/[\w.-]+\.mjs)["'`]/);
|
|
213
|
+
if (m) { clientOut = m[1]; break; }
|
|
214
|
+
}
|
|
215
|
+
} catch (e) {}
|
|
216
|
+
}
|
|
217
|
+
writeFileSync(join(stage, "tools", "build.mjs"), buildMjsTemplate(extId, extRoot, clientOut, ""));
|
|
218
|
+
}
|
|
219
|
+
// tools/deploy.mjs 本身
|
|
220
|
+
if (existsSync(join(extRoot, "tools", "deploy.mjs"))) {
|
|
221
|
+
cpSync(join(extRoot, "tools", "deploy.mjs"), join(stage, "tools", "deploy.mjs"));
|
|
222
|
+
}
|
|
223
|
+
// AGENTS.md:自带优先,否则模板生成
|
|
224
|
+
if (existsSync(join(extRoot, "AGENTS.md"))) cpSync(join(extRoot, "AGENTS.md"), join(stage, "AGENTS.md"));
|
|
225
|
+
else writeFileSync(join(stage, "AGENTS.md"), agentsMdTemplate(extId));
|
|
226
|
+
|
|
227
|
+
// 1.5) _shared 依赖:部署 src 里的源码可能相对引用 ../../../_shared/*(源仓出仓路径)——
|
|
228
|
+
// 部署根放一份 _shared(小体量、纯源码),让现场 build 的模块解析与源仓一致
|
|
229
|
+
const sharedSrc = resolve(extRoot, "..", "_shared");
|
|
230
|
+
if (existsSync(sharedSrc)) {
|
|
231
|
+
const sharedStage = join(stage, "_shared");
|
|
232
|
+
copyTree(sharedSrc, sharedStage, DIR_SKIP);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 2) 落位(staging → 部署目录;package.json/vite.config.ts 双放:根级按 §3.2 结构,src 内供现场阅读)
|
|
236
|
+
for (const t of targets) {
|
|
237
|
+
cpSync(stage, t, { recursive: true });
|
|
238
|
+
for (const f of ["package.json", "vite.config.ts"]) {
|
|
239
|
+
if (existsSync(join(stage, f))) cpSync(join(stage, f), join(t, f));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
log(`dev-pack → ${targets.map((t) => t.replace(homedir(), "~")).join(", ")}(AGENTS.md + src/ + tools/)`);
|
|
243
|
+
} finally {
|
|
244
|
+
rmSync(stage, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiti-claude-code-agent · 受控登录 client(薄壳,paste-relay 模式)。
|
|
3
|
+
*
|
|
4
|
+
* 全部 UI/轮询/文案模板在 ./controlled-login-dialog.mjs
|
|
5
|
+
* (权威源 pi-web-ext/_shared/controlled-login-dialog.mjs 的部署副本,改动须双向同步)——
|
|
6
|
+
* 本文件只传扩展专属配置。
|
|
7
|
+
*/
|
|
8
|
+
import { registerControlledLoginClient } from "./controlled-login-dialog.mjs";
|
|
9
|
+
|
|
10
|
+
export default function register(ctx) {
|
|
11
|
+
registerControlledLoginClient(ctx, {
|
|
12
|
+
extId: "aiti-claude-code-agent",
|
|
13
|
+
toolbarLabel: "登录 Claude Code",
|
|
14
|
+
title: "登录 Claude Code",
|
|
15
|
+
pasteHint:
|
|
16
|
+
"后端发起官方 claude auth login(Claude 订阅档),把授权链接交给你——" +
|
|
17
|
+
"在任意设备的浏览器打开并登录授权后,Claude 的完成页会显示一段一次性 code:" +
|
|
18
|
+
"把它(或整个回调链接)复制到下方输入框提交,后端原样喂给 login 进程即完成登录。" +
|
|
19
|
+
"code 一次性且时效短,请拿到后尽快提交。",
|
|
20
|
+
successHint: null,
|
|
21
|
+
});
|
|
22
|
+
}
|