made-bridge 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/builtin/plugins/README.md +22 -0
- package/builtin/themes/README.md +64 -0
- package/builtin/themes/sunset/theme.json +19 -0
- package/config.example.json +20 -0
- package/demo-project/README.md +5 -0
- package/demo-project/index.mjs +14 -0
- package/package.json +48 -4
- package/public/app.js +1028 -0
- package/public/assistants.html +67 -0
- package/public/bean-icon.png +0 -0
- package/public/i18n-messages.js +967 -0
- package/public/i18n-static.js +615 -0
- package/public/i18n.js +50 -0
- package/public/index.html +149 -0
- package/public/made-icon.png +0 -0
- package/public/plugins.html +64 -0
- package/public/plugins.js +335 -0
- package/public/provider-icon.js +77 -0
- package/public/styles.css +445 -0
- package/public/tasks.html +85 -0
- package/src/agent-icons.mjs +138 -0
- package/src/agents/codex.mjs +94 -0
- package/src/agents/cursor.mjs +31 -0
- package/src/agents/events.mjs +92 -0
- package/src/agents/index.mjs +7 -0
- package/src/agents/qoder.mjs +34 -0
- package/src/agents/shared.mjs +191 -0
- package/src/agents/workbuddy.mjs +121 -0
- package/src/board-link.mjs +266 -0
- package/src/config.mjs +196 -0
- package/src/doctor.mjs +146 -0
- package/src/i18n.mjs +257 -0
- package/src/installed.mjs +291 -0
- package/src/jobs.mjs +369 -0
- package/src/language-settings.mjs +38 -0
- package/src/model-settings.mjs +63 -0
- package/src/openai-key.mjs +47 -0
- package/src/oray.mjs +243 -0
- package/src/pairing.mjs +203 -0
- package/src/plugin-package.mjs +132 -0
- package/src/plugin-runtime.mjs +169 -0
- package/src/plugins/contracts.d.ts +184 -0
- package/src/plugins/enhance.mjs +156 -0
- package/src/plugins/registry.mjs +146 -0
- package/src/providers.mjs +75 -0
- package/src/server.mjs +950 -0
- package/src/sessions.mjs +134 -0
- package/src/speech/index.mjs +10 -0
- package/src/speech/openai.mjs +48 -0
- package/src/speech/whisper.mjs +59 -0
- package/src/speech-settings.mjs +209 -0
- package/src/transcribe.mjs +85 -0
- package/src/tunnel.mjs +531 -0
- package/src/usb-receiver.mjs +664 -0
- package/src/voice-settings.mjs +69 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="theme-color" content="#faf3e7">
|
|
7
|
+
<title data-i18n="static.109">任务 · 码得</title>
|
|
8
|
+
<link rel="icon" type="image/png" sizes="112x112" href="/made-icon.png">
|
|
9
|
+
<link rel="stylesheet" href="/styles.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body data-page="tasks">
|
|
12
|
+
<div class="page-glow" aria-hidden="true"></div>
|
|
13
|
+
<div class="app">
|
|
14
|
+
<header class="topbar">
|
|
15
|
+
<a class="brand" href="/" aria-label="码得大盘" data-i18n-aria-label="static.002">
|
|
16
|
+
<span class="brand-mark" aria-hidden="true"><img src="/made-icon.png" alt="" width="112" height="112"></span>
|
|
17
|
+
<span><span class="brand-name"><span data-i18n="static.003">码得</span></span><span class="brand-sub"><span data-i18n="static.004">made · CODING COMPANION</span></span></span>
|
|
18
|
+
</a>
|
|
19
|
+
<nav class="topnav" aria-label="页面导航" data-i18n-aria-label="static.005"><a href="/"><span data-i18n="static.006">大盘</span></a><a href="/assistants.html"><span data-i18n="static.007">助手</span></a><a class="active" aria-current="page" href="/tasks.html"><span data-i18n="static.008">任务</span></a><a href="/plugins.html"><span data-i18n="static.140">插件</span></a></nav>
|
|
20
|
+
<div class="language-picker"><label for="languageSelect" data-i18n="language.label">语言</label><select id="languageSelect" aria-label="界面语言" data-i18n-aria-label="language.label"><option value="zh-CN">简体中文</option><option value="en">English</option></select></div>
|
|
21
|
+
<div class="top-actions">
|
|
22
|
+
<span id="headerConnection" class="connection-pill"><span class="dot"></span><span><span data-i18n="static.009">正在连接</span></span></span>
|
|
23
|
+
<button id="refreshButton" class="icon-button" type="button" title="刷新状态" aria-label="刷新状态" data-i18n-title="static.010" data-i18n-aria-label="static.010"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 1 0-2.3 6.1"/><path d="M20 5v6h-6"/></svg></button>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<main id="top">
|
|
28
|
+
<section class="hero task-hero" aria-labelledby="hero-title">
|
|
29
|
+
<div><span class="eyebrow"><span data-i18n="static.110">TASKS · CODING AGENT</span></span><h1 id="hero-title"><span data-i18n="static.111">说出任务,</span><em><span data-i18n="static.112">看见进度</span></em></h1><p class="hero-desc"><span data-i18n="static.113">点“新建任务”选择助手和代码文件夹。第一句语音或文字会成为任务标题,后续指令都在这个任务里继续;每次执行前仍需确认。</span></p></div>
|
|
30
|
+
</section>
|
|
31
|
+
<div class="tasks-workspace">
|
|
32
|
+
<section class="panel sessions-panel" aria-labelledby="sessions-title">
|
|
33
|
+
<div class="panel-head"><div><h2 id="sessions-title"><span data-i18n="static.008">任务</span></h2><p class="panel-intro"><span data-i18n="static.114">选择一个任务,查看最新进展并继续对话。</span></p></div><button id="newSessionButton" class="small-button confirm" type="button" aria-expanded="false" aria-controls="sessionForm"><span data-i18n="static.115">+ 新建任务</span></button></div>
|
|
34
|
+
<form id="sessionForm" class="session-create-form" hidden autocomplete="off">
|
|
35
|
+
<label class="compose-field" for="provider"><span class="field-label"><span data-i18n="static.097">编程助手</span></span><select id="provider" name="provider" required><option value="" data-i18n="static.116">正在加载…</option></select></label>
|
|
36
|
+
<label class="compose-field" for="project"><span class="field-label"><span data-i18n="static.117">代码文件夹</span></span><select id="project" name="projectId" required><option value="" data-i18n="static.116">正在加载…</option></select></label>
|
|
37
|
+
<p class="panel-intro"><span data-i18n="static.118">第一句指令会自动成为任务标题,之后可以改标题。</span></p>
|
|
38
|
+
<p id="sessionContextNotice" class="panel-intro"><span data-i18n="static.119">Codex、Cursor、Qoder 将尽可能沿用各自 CLI 的任务上下文。</span></p>
|
|
39
|
+
<button id="createSessionButton" class="primary-button" type="submit" disabled><span data-i18n="static.120">创建任务</span></button>
|
|
40
|
+
<p id="sessionFormMessage" class="form-message" role="status" aria-live="polite"></p>
|
|
41
|
+
</form>
|
|
42
|
+
<div id="sessionList" class="session-list" aria-label="任务列表" data-i18n-aria-label="static.121"><div class="empty"><strong><span data-i18n="static.122">正在读取任务</span></strong></div></div>
|
|
43
|
+
<section id="legacySessions" class="legacy-sessions" aria-label="历史记录" hidden data-i18n-aria-label="static.123">
|
|
44
|
+
<button id="legacySessionToggle" class="legacy-session-toggle" type="button" aria-expanded="false" aria-controls="legacySessionList"><span data-i18n="static.123">历史记录</span></button>
|
|
45
|
+
<div id="legacySessionList" class="session-list legacy-session-list" aria-label="历史任务列表" hidden data-i18n-aria-label="static.124"></div>
|
|
46
|
+
</section>
|
|
47
|
+
</section>
|
|
48
|
+
<div class="conversation-column">
|
|
49
|
+
<section class="panel conversation-head" aria-labelledby="currentSessionTitle">
|
|
50
|
+
<div class="conversation-heading"><div><span class="section-kicker"><span data-i18n="static.125">当前任务</span></span><h2 id="currentSessionTitle"><span data-i18n="static.126">请选择任务</span></h2><p id="currentSessionMeta" class="panel-intro"><span data-i18n="static.127">选择左侧任务,或新建一个任务。</span></p></div><button id="renameSessionButton" class="small-button" type="button" hidden><span data-i18n="static.128">改标题</span></button></div>
|
|
51
|
+
<p id="currentSessionWarning" class="session-context-warning" hidden></p>
|
|
52
|
+
<form id="renameSessionForm" class="rename-session-form" hidden autocomplete="off"><label class="field-label" for="renameSessionTitle"><span data-i18n="static.129">新标题</span></label><div class="rename-session-row"><input id="renameSessionTitle" class="secret-input" type="text" maxlength="80" required><button id="renameSessionSubmit" class="primary-button" type="submit"><span data-i18n="static.130">保存</span></button><button id="renameSessionCancel" class="small-button" type="button"><span data-i18n="static.131">取消</span></button></div><p id="renameSessionMessage" class="form-message" role="status" aria-live="polite"></p></form>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section class="panel queue" aria-labelledby="queue-title">
|
|
56
|
+
<div class="panel-head"><div><h2 id="queue-title"><span data-i18n="static.132">最新进展</span></h2><p class="panel-intro"><span data-i18n="static.133">显示这个任务最新一次指令、执行状态和助手回复。</span></p></div></div>
|
|
57
|
+
<div id="jobList" class="job-list" aria-live="polite"><div class="empty"><div class="empty-icon" aria-hidden="true">⌁</div><strong><span data-i18n="static.122">正在读取任务</span></strong><span><span data-i18n="static.134">正在连接本机桥接服务…</span></span></div></div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section class="panel composer" aria-labelledby="compose-title">
|
|
61
|
+
<div class="composer-top"><div><h2 id="compose-title"><span data-i18n="static.135">继续这个任务</span></h2><p class="panel-intro"><span data-i18n="static.136">下一句会接着当前任务的上下文继续。</span></p></div></div>
|
|
62
|
+
<form id="jobForm">
|
|
63
|
+
<label class="compose-field" for="instruction"><span class="field-label"><span data-i18n="static.137">下一条指令</span></span><textarea id="instruction" name="instruction" maxlength="2000" placeholder="例如:接着修复刚才发现的第一个问题。" required data-i18n-placeholder="static.138"></textarea></label>
|
|
64
|
+
<div class="compose-actions"><button id="createButton" class="primary-button" type="submit" disabled><span><span data-i18n="static.139">发送并回显</span></span><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m4 10 11-6-3 12-2.4-4.2L4 10Z"/><path d="m9.6 11.8 5.4-7.8"/></svg></button></div>
|
|
65
|
+
<p id="formMessage" class="form-message" role="status" aria-live="polite"></p>
|
|
66
|
+
</form>
|
|
67
|
+
</section>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</main>
|
|
71
|
+
<footer class="footer"><span><span data-i18n="static.082">码得 · 多助手桥接</span></span><span id="lastUpdated"><span data-i18n="static.083">等待首次同步</span></span></footer>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
74
|
+
<div id="pairingOverlay" class="pairing-overlay" hidden>
|
|
75
|
+
<section class="pairing-dialog" role="dialog" aria-modal="true" aria-labelledby="pairingDialogTitle" aria-describedby="pairingDialogDescription">
|
|
76
|
+
<div class="pairing-dialog-top"><div><span class="section-kicker"><span data-i18n="static.084">设备请求连接</span></span><h2 id="pairingDialogTitle"><span data-i18n="static.085">确认码得配对</span></h2></div><button id="pairingDismissButton" class="icon-button" type="button" title="稍后处理" aria-label="稍后处理" data-i18n-title="static.086" data-i18n-aria-label="static.086">×</button></div>
|
|
77
|
+
<p id="pairingDialogDescription"><span data-i18n="static.087">请核对下方数字是否与码得显示的一致。只有你确认后,这台设备才能控制电脑上的编程助手。</span></p>
|
|
78
|
+
<strong id="pairingCode" class="pairing-code" aria-label="配对验证码" data-i18n-aria-label="static.088"><span data-i18n="static.089">000000</span></strong>
|
|
79
|
+
<span id="pairingRemaining" class="pairing-remaining"></span>
|
|
80
|
+
<div class="pairing-dialog-actions"><button id="pairingRejectButton" class="small-button" type="button"><span data-i18n="static.090">拒绝连接</span></button><button id="pairingConfirmButton" class="primary-button" type="button"><span data-i18n="static.091">数字一致,确认连接</span></button></div>
|
|
81
|
+
</section>
|
|
82
|
+
</div>
|
|
83
|
+
<script type="module" src="/app.js"></script>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, self-contained avatars shared by the desktop and the round display.
|
|
3
|
+
* Pixels are row-major 4-bit palette indices: high nibble first, no row padding.
|
|
4
|
+
* There are no image URLs, executable SVG or firmware-specific animal names on
|
|
5
|
+
* the wire. Legacy preset names are resolved only inside this bridge module.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_ICON_SIZE = 48;
|
|
8
|
+
export const MAX_ICON_JSON_BYTES = 2048;
|
|
9
|
+
const RGBA = /^[0-9a-f]{8}$/i;
|
|
10
|
+
const RGB = /^#[0-9a-f]{6}$/i;
|
|
11
|
+
const FIELDS = new Set(['format', 'width', 'height', 'palette', 'data', 'background', 'accent']);
|
|
12
|
+
const validated = new WeakSet();
|
|
13
|
+
|
|
14
|
+
function dimensions(width, height) {
|
|
15
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_ICON_SIZE || height > MAX_ICON_SIZE) {
|
|
16
|
+
throw new Error(`图标尺寸必须为 1–${MAX_ICON_SIZE} 像素`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateIndexedIcon(icon) {
|
|
21
|
+
if (icon && validated.has(icon)) return icon;
|
|
22
|
+
if (!icon || typeof icon !== 'object' || Array.isArray(icon) || icon.format !== 'indexed4') throw new Error('图标必须为 indexed4 像素对象');
|
|
23
|
+
if (Object.keys(icon).some(key => !FIELDS.has(key))) throw new Error('图标包含不支持的字段');
|
|
24
|
+
dimensions(icon.width, icon.height);
|
|
25
|
+
if (!Array.isArray(icon.palette) || icon.palette.length < 1 || icon.palette.length > 16 || Array.from(icon.palette).some(color => typeof color !== 'string' || !RGBA.test(color))) {
|
|
26
|
+
throw new Error('图标 palette 需要 1–16 个 RRGGBBAA 颜色');
|
|
27
|
+
}
|
|
28
|
+
if (typeof icon.background !== 'string' || !RGB.test(icon.background) || typeof icon.accent !== 'string' || !RGB.test(icon.accent)) throw new Error('图标背景与强调色必须为 #RRGGBB');
|
|
29
|
+
const pixels = icon.width * icon.height;
|
|
30
|
+
const bytes = Math.ceil(pixels / 2);
|
|
31
|
+
if (typeof icon.data !== 'string' || icon.data.length !== Math.ceil(bytes / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(icon.data)) throw new Error('图标 data 的 Base64 长度或格式无效');
|
|
32
|
+
const packed = Buffer.from(icon.data, 'base64');
|
|
33
|
+
if (packed.length !== bytes || packed.toString('base64') !== icon.data) throw new Error('图标 data 必须使用标准 Base64 编码');
|
|
34
|
+
for (let i = 0; i < pixels; i++) {
|
|
35
|
+
const index = i % 2 ? packed[i >> 1] & 15 : packed[i >> 1] >> 4;
|
|
36
|
+
if (index >= icon.palette.length) throw new Error('图标像素引用了不存在的 palette 颜色');
|
|
37
|
+
}
|
|
38
|
+
if (pixels % 2 && (packed.at(-1) & 15)) throw new Error('图标最后一个未使用的低半字节必须为零');
|
|
39
|
+
const normalized = Object.freeze({
|
|
40
|
+
format: 'indexed4', width: icon.width, height: icon.height,
|
|
41
|
+
palette: Object.freeze(icon.palette.map(color => color.toLowerCase())), data: icon.data,
|
|
42
|
+
background: icon.background.toLowerCase(), accent: icon.accent.toLowerCase(),
|
|
43
|
+
});
|
|
44
|
+
if (Buffer.byteLength(JSON.stringify(normalized)) > MAX_ICON_JSON_BYTES) throw new Error('图标数据超过 2 KB');
|
|
45
|
+
validated.add(normalized);
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Convert bounded raw RGBA bytes to an avatar; deterministic median-cut when
|
|
50
|
+
* the source contains more than 16 colors. Use a developer image tool to resize
|
|
51
|
+
* a PNG first; the running bridge does not decode arbitrary image files. */
|
|
52
|
+
export function rgbaToIndexed4({ width, height, pixels, background = '#25373f', accent = '#b7e3df' }) {
|
|
53
|
+
dimensions(width, height);
|
|
54
|
+
if (!(pixels instanceof Uint8Array) || pixels.length !== width * height * 4) throw new Error('RGBA 数据长度必须为 width × height × 4');
|
|
55
|
+
const colors = new Map();
|
|
56
|
+
const source = [];
|
|
57
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
58
|
+
const rgba = pixels[i + 3] === 0 ? [0, 0, 0, 0] : [...pixels.subarray(i, i + 4)];
|
|
59
|
+
const key = rgba.join(',');
|
|
60
|
+
if (!colors.has(key)) colors.set(key, { rgba, count: 0 });
|
|
61
|
+
colors.get(key).count++;
|
|
62
|
+
source.push(key);
|
|
63
|
+
}
|
|
64
|
+
let boxes = [[...colors.values()]];
|
|
65
|
+
const span = (box, channel) => Math.max(...box.map(c => c.rgba[channel])) - Math.min(...box.map(c => c.rgba[channel]));
|
|
66
|
+
while (boxes.length < 16) {
|
|
67
|
+
let candidate = -1, channel = 0, score = -1;
|
|
68
|
+
boxes.forEach((box, i) => {
|
|
69
|
+
if (box.length < 2) return;
|
|
70
|
+
for (let c = 0; c < 4; c++) {
|
|
71
|
+
const next = span(box, c) * Math.sqrt(box.reduce((sum, color) => sum + color.count, 0));
|
|
72
|
+
if (next > score) { score = next; candidate = i; channel = c; }
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (candidate < 0) break;
|
|
76
|
+
const box = boxes[candidate].sort((a, b) => a.rgba[channel] - b.rgba[channel]);
|
|
77
|
+
const half = box.reduce((sum, color) => sum + color.count, 0) / 2;
|
|
78
|
+
let total = 0, split = 1;
|
|
79
|
+
for (let i = 0; i < box.length - 1; i++) { total += box[i].count; split = i + 1; if (total >= half) break; }
|
|
80
|
+
boxes.splice(candidate, 1, box.slice(0, split), box.slice(split));
|
|
81
|
+
}
|
|
82
|
+
const palette = [], indexes = new Map();
|
|
83
|
+
for (const box of boxes) {
|
|
84
|
+
const weight = box.reduce((sum, color) => sum + color.count, 0);
|
|
85
|
+
const rgba = [0, 1, 2, 3].map(channel => Math.round(box.reduce((sum, color) => sum + color.rgba[channel] * color.count, 0) / weight));
|
|
86
|
+
for (const color of box) indexes.set(color.rgba.join(','), palette.length);
|
|
87
|
+
palette.push(rgba.map(value => value.toString(16).padStart(2, '0')).join(''));
|
|
88
|
+
}
|
|
89
|
+
const packed = Buffer.alloc(Math.ceil(source.length / 2));
|
|
90
|
+
source.forEach((key, i) => { packed[i >> 1] |= indexes.get(key) << (i % 2 ? 0 : 4); });
|
|
91
|
+
return validateIndexedIcon({ format: 'indexed4', width, height, palette, data: packed.toString('base64'), background, accent });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Original 42 × 42 artwork is maintained here, never inside device firmware.
|
|
95
|
+
const artwork = {
|
|
96
|
+
fox: { background: '#503728', accent: '#ffb264', shapes: [
|
|
97
|
+
[6,3,12,18,'#d87935',3], [24,3,12,18,'#d87935',3], [9,7,6,9,'#ffcca7',3], [27,7,6,9,'#ffcca7',3], [5,12,32,26,'#ffac5b',16], [9,26,24,11,'#fff1df',8], [13,21,3,4,'#26364b',3], [26,21,3,4,'#26364b',3], [19,30,5,4,'#664343',3],
|
|
98
|
+
] },
|
|
99
|
+
rabbit: { background: '#413756', accent: '#c5abff', shapes: [
|
|
100
|
+
[9,1,9,24,'#f3ebff',6], [24,1,9,24,'#f3ebff',6], [12,5,3,14,'#e6a9d5',3], [27,5,3,14,'#e6a9d5',3], [6,16,30,23,'#f3ebff',14], [13,25,3,4,'#35354d',3], [26,25,3,4,'#35354d',3], [19,31,5,4,'#d586ba',3],
|
|
101
|
+
] },
|
|
102
|
+
owl: { background: '#254b54', accent: '#60ddd2', shapes: [
|
|
103
|
+
[6,5,11,18,'#238d98',4], [25,5,11,18,'#238d98',4], [5,11,32,28,'#4cc9c7',14], [8,19,12,13,'#fff9e9',7], [22,19,12,13,'#fff9e9',7], [13,23,4,5,'#214e64',3], [26,23,4,5,'#214e64',3], [19,31,5,5,'#ffc269',2],
|
|
104
|
+
] },
|
|
105
|
+
panda: { background: '#36445d', accent: '#d7e4f5', shapes: [
|
|
106
|
+
[4,7,12,12,'#293a54',6], [26,7,12,12,'#293a54',6], [6,12,30,26,'#f5f8ff',15], [10,20,10,12,'#293a54',6], [22,20,10,12,'#293a54',6], [14,23,3,4,'#f5f8ff',3], [26,23,3,4,'#f5f8ff',3], [19,31,5,4,'#293a54',3],
|
|
107
|
+
] },
|
|
108
|
+
generic: { background: '#25373f', accent: '#b7e3df', shapes: [
|
|
109
|
+
[5,8,32,24,'#b7e3df',9], [10,25,9,11,'#b7e3df',2], [11,17,4,5,'#25373f',2], [19,17,4,5,'#25373f',2], [27,17,4,5,'#25373f',2],
|
|
110
|
+
] },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function renderArtwork({ background, accent, shapes }) {
|
|
114
|
+
const size = 48, pixels = new Uint8Array(size * size * 4);
|
|
115
|
+
for (const [x, y, width, height, fill, round] of shapes) {
|
|
116
|
+
const rx = Math.min(round, width / 2), ry = Math.min(round, height / 2);
|
|
117
|
+
const color = [1, 3, 5].map(offset => parseInt(fill.slice(offset, offset + 2), 16));
|
|
118
|
+
for (let py = 0; py < size; py++) for (let px = 0; px < size; px++) {
|
|
119
|
+
const dx = (px + .5) * 42 / size - x, dy = (py + .5) * 42 / size - y;
|
|
120
|
+
if (dx < 0 || dy < 0 || dx >= width || dy >= height) continue;
|
|
121
|
+
const cx = Math.max(rx - dx, dx - (width - rx), 0), cy = Math.max(ry - dy, dy - (height - ry), 0);
|
|
122
|
+
if (cx && cy && (cx / rx) ** 2 + (cy / ry) ** 2 > 1) continue;
|
|
123
|
+
pixels.set([...color, 255], (py * size + px) * 4);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return rgbaToIndexed4({ width: size, height: size, pixels, background, accent });
|
|
127
|
+
}
|
|
128
|
+
export const builtinAgentIcons = Object.freeze(Object.fromEntries(Object.entries(artwork).map(([id, image]) => [id, renderArtwork(image)])));
|
|
129
|
+
|
|
130
|
+
/** Legacy string aliases are accepted for existing desktop plugins only. */
|
|
131
|
+
export function normalizeAgentIcon(icon) {
|
|
132
|
+
if (icon === undefined || icon === null) return builtinAgentIcons.generic;
|
|
133
|
+
if (typeof icon === 'string') {
|
|
134
|
+
if (!Object.hasOwn(builtinAgentIcons, icon)) throw new Error('未知的旧图标名称;请提供 indexed4 像素对象');
|
|
135
|
+
return builtinAgentIcons[icon];
|
|
136
|
+
}
|
|
137
|
+
return validateIndexedIcon(icon);
|
|
138
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
5
|
+
import { probeCli, runCli, gitState } from './shared.mjs';
|
|
6
|
+
import { jsonLineEvents, toolActivity } from './events.mjs';
|
|
7
|
+
|
|
8
|
+
// `codex login status` 只认 ChatGPT/OAuth 登录。当用户在 ~/.codex/config.toml 里配置了
|
|
9
|
+
// 自定义 model_provider(experimental_bearer_token 或 env_key 指向的环境变量)时,
|
|
10
|
+
// CLI 实际可用但登录状态仍显示未登录。这里补充识别这类鉴权方式,避免误报“请先登录”。
|
|
11
|
+
function hasCustomProviderAuth() {
|
|
12
|
+
try {
|
|
13
|
+
const config = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8');
|
|
14
|
+
const bearer = config.match(/experimental_bearer_token\s*=\s*"([^"]+)"/);
|
|
15
|
+
if (bearer?.[1]) return true;
|
|
16
|
+
const envKey = config.match(/env_key\s*=\s*"([^"]+)"/);
|
|
17
|
+
return Boolean(envKey?.[1] && process.env[envKey[1]]);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
|
|
25
|
+
export function createEvents(onProgress, onSession) {
|
|
26
|
+
return jsonLineEvents((event, state) => {
|
|
27
|
+
const sessionId = event.type === 'thread.started' ? event.thread_id : undefined;
|
|
28
|
+
if (event.item?.type === 'agent_message' && event.type === 'item.completed' && typeof event.item.text === 'string') state.lastMessage = event.item.text;
|
|
29
|
+
if (event.type === 'turn.failed' || event.type === 'error') state.terminalError = '编程工具报告任务失败';
|
|
30
|
+
let activity = '';
|
|
31
|
+
if (event.type === 'turn.started') activity = '正在分析任务';
|
|
32
|
+
if (event.type === 'turn.completed') activity = '正在整理结果';
|
|
33
|
+
if (event.type.startsWith('item.')) {
|
|
34
|
+
const completed = event.type === 'item.completed';
|
|
35
|
+
switch (event.item?.type) {
|
|
36
|
+
case 'command_execution': activity = toolActivity('command', completed); break;
|
|
37
|
+
case 'file_change': activity = toolActivity('edit', completed); break;
|
|
38
|
+
case 'web_search': activity = completed ? '资料检索完成' : '正在查找资料'; break;
|
|
39
|
+
case 'mcp_tool_call': case 'collab_tool_call': activity = toolActivity('', completed); break;
|
|
40
|
+
case 'todo_list': if (completed) activity = '已更新任务步骤'; break;
|
|
41
|
+
case 'agent_message': if (completed) activity = '正在整理答复'; break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { sessionId, activity };
|
|
45
|
+
}, onProgress, onSession);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const adapter = {
|
|
49
|
+
label: 'Codex', createEvents,
|
|
50
|
+
command({ cwd, instruction, config, model, sessionId }) {
|
|
51
|
+
const args = ['exec', '--json', '--sandbox', 'workspace-write', '-C', cwd];
|
|
52
|
+
if (sessionId) args.push('resume');
|
|
53
|
+
const selected = model ?? config.codexModel;
|
|
54
|
+
if (selected) args.push('-m', selected);
|
|
55
|
+
if (sessionId) args.push(sessionId);
|
|
56
|
+
args.push('-');
|
|
57
|
+
return { command: 'codex', args, stdin: instruction };
|
|
58
|
+
},
|
|
59
|
+
classifyExit({ job, project, stderr, output, startingGitSnapshot }) {
|
|
60
|
+
const activeWriter = /Failed to create session:\s*thread-store conflict:\s*thread [A-Za-z0-9_-]{6,128} already has an active writer/;
|
|
61
|
+
if (job.sessionId && activeWriter.test(`${stderr}\n${output.rawTail}`)) {
|
|
62
|
+
let unchanged = false;
|
|
63
|
+
try { unchanged = gitState(project.path).snapshot === startingGitSnapshot; }
|
|
64
|
+
catch { /* An unreadable or changed repository cannot be retried safely. */ }
|
|
65
|
+
if (!unchanged) return new Error('Codex 对话正被使用,项目文件也发生变化。请先检查改动。');
|
|
66
|
+
const error = new Error('此 Codex 对话正在另一窗口或进程使用;结束后再次确认即可继续');
|
|
67
|
+
error.code = 'CODEX_SESSION_ACTIVE_WRITER';
|
|
68
|
+
error.retryable = true;
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
71
|
+
if (/model is not supported when using Codex|Model metadata for/.test(output.rawTail)) {
|
|
72
|
+
return new Error('Codex 当前模型不能用于这个登录账号。请在电脑桥接页面的“编程助手模型”中选择该账号可用的模型。');
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
apiVersion: 1, kind: 'coding-agent', id: 'codex', label: 'Codex', icon: builtinAgentIcons.fox,
|
|
79
|
+
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
80
|
+
model: { default: 'gpt-6-astra', required: true },
|
|
81
|
+
sessionIdPattern: UUID,
|
|
82
|
+
probe: () => {
|
|
83
|
+
const state = probeCli('codex', ['login', 'status']);
|
|
84
|
+
if (state.available || !hasCustomProviderAuth()) return state;
|
|
85
|
+
return { available: true, mode: 'local_cli', reason: '' };
|
|
86
|
+
},
|
|
87
|
+
run: (context) => runCli(context, adapter),
|
|
88
|
+
createEvents,
|
|
89
|
+
syncJobState(job) {
|
|
90
|
+
if (job.error === 'Codex 当前模型不能用于这个登录账号。请设置 VIBE_CODEX_MODEL 为该账号在 CLI 中可用的模型。') {
|
|
91
|
+
job.error = 'Codex 当前模型不能用于这个登录账号。请在电脑桥接页面的“编程助手模型”中选择该账号可用的模型。';
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
2
|
+
import { probeCli, runCli } from './shared.mjs';
|
|
3
|
+
import { jsonLineEvents, assistantEvent, streamActivity } from './events.mjs';
|
|
4
|
+
|
|
5
|
+
export function createEvents(onProgress, onSession) {
|
|
6
|
+
return jsonLineEvents((event, state) => {
|
|
7
|
+
assistantEvent(event, state, { append: true });
|
|
8
|
+
return { sessionId: event.session_id, activity: streamActivity(event) };
|
|
9
|
+
}, onProgress, onSession);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const adapter = {
|
|
13
|
+
label: 'Cursor', createEvents,
|
|
14
|
+
command({ cwd, instruction, model, sessionId }) {
|
|
15
|
+
const args = ['--print', '--trust', '--output-format', 'stream-json', '--stream-partial-output', '--sandbox', 'enabled', '--auto-review', '--workspace', cwd];
|
|
16
|
+
if (sessionId) args.push('--resume', sessionId);
|
|
17
|
+
if (model) args.push('--model', model);
|
|
18
|
+
args.push(instruction);
|
|
19
|
+
return { command: 'cursor-agent', args };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
apiVersion: 1, kind: 'coding-agent', id: 'cursor', label: 'Cursor', icon: builtinAgentIcons.rabbit,
|
|
25
|
+
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
26
|
+
model: { default: '', required: false },
|
|
27
|
+
sessionIdPattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
28
|
+
probe: () => probeCli('cursor-agent', ['status']),
|
|
29
|
+
run: (context) => runCli(context, adapter),
|
|
30
|
+
createEvents,
|
|
31
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export function toolActivity(name, completed = false) {
|
|
2
|
+
const tool = String(name || '').toLowerCase();
|
|
3
|
+
if (/read|view|open/.test(tool)) return completed ? '文件读取完成' : '正在读取文件';
|
|
4
|
+
if (/write|edit|patch|apply|delete/.test(tool)) return completed ? '文件修改完成' : '正在修改文件';
|
|
5
|
+
if (/terminal|shell|bash|command|exec/.test(tool)) return completed ? '命令执行完成' : '正在运行命令';
|
|
6
|
+
if (/search|grep|glob|find/.test(tool)) return completed ? '代码检索完成' : '正在检索代码';
|
|
7
|
+
return completed ? '工具操作完成' : '正在使用工具';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function textBlocks(content) {
|
|
11
|
+
if (typeof content === 'string') return content;
|
|
12
|
+
if (!Array.isArray(content)) return '';
|
|
13
|
+
return content.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
14
|
+
.map((part) => part.text).join('');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function streamActivity(event) {
|
|
18
|
+
if (event.type === 'system' && event.subtype === 'init') return '已连接编程工具';
|
|
19
|
+
if (event.type === 'tool_call') {
|
|
20
|
+
const name = Object.keys(event.tool_call || {})[0];
|
|
21
|
+
return toolActivity(name, event.subtype === 'completed');
|
|
22
|
+
}
|
|
23
|
+
if (event.type === 'stream_event') {
|
|
24
|
+
const inner = event.event;
|
|
25
|
+
if (inner?.type === 'content_block_start' && inner.content_block?.type === 'tool_use') {
|
|
26
|
+
return toolActivity(inner.content_block.name);
|
|
27
|
+
}
|
|
28
|
+
if (inner?.type === 'content_block_delta' && inner.delta?.type === 'text_delta') return '正在整理答复';
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
if (event.type === 'assistant') {
|
|
32
|
+
const toolBlock = event.message?.content?.find?.((part) => part?.type === 'tool_use');
|
|
33
|
+
return toolBlock ? toolActivity(toolBlock.name) : '正在整理答复';
|
|
34
|
+
}
|
|
35
|
+
if (event.type === 'result' && event.subtype === 'success') return '正在整理结果';
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only categorical activity is emitted by builtin adapters; raw tool arguments
|
|
40
|
+
// and reasoning never become the device progress text.
|
|
41
|
+
export function jsonLineEvents(interpret, onProgress = () => {}, onSession = () => {}) {
|
|
42
|
+
let pending = '';
|
|
43
|
+
let rawTail = '';
|
|
44
|
+
let discardingOversized = false;
|
|
45
|
+
let sawEvent = false;
|
|
46
|
+
const state = { lastMessage: '', assistantDeltas: '', finalResult: '', terminalError: '' };
|
|
47
|
+
const consume = (line) => {
|
|
48
|
+
let event;
|
|
49
|
+
try { event = JSON.parse(line); } catch { return; }
|
|
50
|
+
if (!event || typeof event !== 'object' || typeof event.type !== 'string') return;
|
|
51
|
+
sawEvent = true;
|
|
52
|
+
const update = interpret(event, state) || {};
|
|
53
|
+
if (typeof update.sessionId === 'string') onSession(update.sessionId);
|
|
54
|
+
if (update.activity) onProgress(update.activity);
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
write(chunk) {
|
|
58
|
+
const text = String(chunk);
|
|
59
|
+
rawTail = `${rawTail}${text}`.slice(-8000);
|
|
60
|
+
pending += text;
|
|
61
|
+
let newline;
|
|
62
|
+
while ((newline = pending.indexOf('\n')) !== -1) {
|
|
63
|
+
const line = pending.slice(0, newline);
|
|
64
|
+
pending = pending.slice(newline + 1);
|
|
65
|
+
if (!discardingOversized && line.length <= 1_000_000) consume(line);
|
|
66
|
+
discardingOversized = false;
|
|
67
|
+
}
|
|
68
|
+
if (pending.length > 1_000_000) { pending = ''; discardingOversized = true; }
|
|
69
|
+
},
|
|
70
|
+
finish() {
|
|
71
|
+
if (!discardingOversized && pending.trim()) consume(pending);
|
|
72
|
+
return {
|
|
73
|
+
result: (state.finalResult || state.lastMessage || (sawEvent ? '任务已执行,未返回文字摘要。' : rawTail.trim())).slice(-8000),
|
|
74
|
+
hasAssistantText: Boolean(state.finalResult || state.lastMessage || !sawEvent),
|
|
75
|
+
error: state.terminalError, rawTail,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assistantEvent(event, state, { append = false } = {}) {
|
|
82
|
+
if (event.type === 'assistant') {
|
|
83
|
+
const text = textBlocks(event.message?.content);
|
|
84
|
+
if (text) {
|
|
85
|
+
state.assistantDeltas = append ? `${state.assistantDeltas}${text}`.slice(-8000) : text;
|
|
86
|
+
state.lastMessage = state.assistantDeltas;
|
|
87
|
+
}
|
|
88
|
+
} else if (event.type === 'result') {
|
|
89
|
+
if (typeof event.result === 'string') state.finalResult = event.result;
|
|
90
|
+
if (event.is_error || (event.subtype && event.subtype !== 'success')) state.terminalError = '编程工具报告任务失败';
|
|
91
|
+
} else if (event.type === 'turn.failed') state.terminalError = '编程工具报告任务失败';
|
|
92
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import codex from './codex.mjs';
|
|
2
|
+
import cursor from './cursor.mjs';
|
|
3
|
+
import qoder from './qoder.mjs';
|
|
4
|
+
import workbuddy from './workbuddy.mjs';
|
|
5
|
+
|
|
6
|
+
export const builtinAgents = Object.freeze([codex, cursor, qoder, workbuddy]);
|
|
7
|
+
export function builtinAgent(id) { return builtinAgents.find((plugin) => plugin.id === id); }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { builtinAgentIcons } from '../agent-icons.mjs';
|
|
2
|
+
import { probeCli, runCli } from './shared.mjs';
|
|
3
|
+
import { jsonLineEvents, assistantEvent, streamActivity } from './events.mjs';
|
|
4
|
+
|
|
5
|
+
export function createEvents(onProgress, onSession) {
|
|
6
|
+
return jsonLineEvents((event, state) => {
|
|
7
|
+
assistantEvent(event, state);
|
|
8
|
+
return {
|
|
9
|
+
sessionId: event.type === 'system' && event.subtype === 'init' ? event.session_id || event.data?.session_id : undefined,
|
|
10
|
+
activity: streamActivity(event),
|
|
11
|
+
};
|
|
12
|
+
}, onProgress, onSession);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const adapter = {
|
|
16
|
+
label: 'Qoder', createEvents,
|
|
17
|
+
command({ cwd, instruction, model, sessionId }) {
|
|
18
|
+
const args = ['--print', '--output-format', 'stream-json', '--permission-mode', 'accept_edits', '--cwd', cwd];
|
|
19
|
+
if (sessionId) args.push('--session-id', sessionId);
|
|
20
|
+
if (model) args.push('--model', model);
|
|
21
|
+
args.push(instruction);
|
|
22
|
+
return { command: 'qoder', args };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
apiVersion: 1, kind: 'coding-agent', id: 'qoder', label: 'Qoder', icon: builtinAgentIcons.owl,
|
|
28
|
+
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
29
|
+
model: { default: '', required: false },
|
|
30
|
+
sessionIdPattern: /^[A-Za-z0-9_-]{6,128}$/,
|
|
31
|
+
probe: () => probeCli('qoder'),
|
|
32
|
+
run: (context) => runCli(context, adapter),
|
|
33
|
+
createEvents,
|
|
34
|
+
};
|