mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* chore(agent): 增加对 Qoder CLI 的支持和识别 - 在 QUICKSTART 文档中添加 Qoder CLI 为可选本地 agent CLI - 更新代码中 agents.ts 注释包含 Qoder CLI 扫描支持 - 修改首次加载时检测的可用 CLI 列表,加入 Qoder CLI - 在多个语言版本的 README 中增加 Qoder CLI 支持及相关徽章统计 - 更新 agent 适配器与事件解析相关的代码注释和文档,包含 qoder-stream-json 解析器 - 调整 Windows 下 spawn 行为以支持 Qoder CLI 的 stdin 提供 prompt - 修复多语言文档对支持的 CLI 数量描述错误,确保数据保持同步 Change-Id: I388f2f61c60ce8faa7cef5d84eb407950f8bdbfb Co-developed-by: Qoder <noreply@qoder.com> * chore(agent): 增加对 Qoder CLI 的支持和识别 - 在 QUICKSTART 文档中添加 Qoder CLI 为可选本地 agent CLI - 更新代码中 agents.ts 注释包含 Qoder CLI 扫描支持 - 修改首次加载时检测的可用 CLI 列表,加入 Qoder CLI - 在多个语言版本的 README 中增加 Qoder CLI 支持及相关徽章统计 - 更新 agent 适配器与事件解析相关的代码注释和文档,包含 qoder-stream-json 解析器 - 调整 Windows 下 spawn 行为以支持 Qoder CLI 的 stdin 提供 prompt - 修复多语言文档对支持的 CLI 数量描述错误,确保数据保持同步 Change-Id: Id33f125b7c0b1a1c0b0274073da74d1578c324f7 Co-developed-by: Qoder <noreply@qoder.com> * feat(agent-icon): 添加新的Qoder徽标SVG图形组件 - 新增qoderGlyph函数,返回指定大小的SVG格式图形 - 图形包含多路径定义,颜色使用深灰和绿色填充 - 该组件可用于替代或补充现有AgentIcon图标功能 - 提升应用程序的品牌标识和视觉表现力 Change-Id: I4eca18166b5e33bc6229b40b2531d5a54607a560 Co-developed-by: Qoder <noreply@qoder.com> * Translate to English: --- **docs(readme): update to expand CLI agents to 16** - Increased the number of coding agent CLIs from 11 to 16 - New agents included: Devin for Terminal, Kiro CLI, Kilo, Mistral Vibe CLI, DeepSeek TUI **docs(readme): update to expand supported coding agents to 16** - Increased the number of supported code agent CLIs from 11 to 16 - Added support for new CLI tools: Devin for Terminal, Kiro CLI, Kilo, Mistral Vibe CLI, DeepSeek CLI - Added automatic CLI detection and switching while maintaining support for more agents - Added BYOK proxy TUI - Expanded compatibility and support coverage in the README’s multiple language versions - Reflected changes across all README translations (Arabic, German, French, Japanese, Korean) - Updated badges and descriptions to reflect CLI count and feature changes - Added event parsers and protocols for the new CLIs in the agent transport implementation - Updated the BYOK proxy and tool exploration features to be compatible with the expanded CLIs Change-Id: I89786b4a0b09bd279fb23265c2177076206fc5af Co-developed-by: Qoder <noreply@qoder.com> * feat(daemon): 支持 imagePaths 参数作为附件路径传递给 Qoder - 修改 buildArgs 函数,添加 --attachment 参数处理 imagePaths 中的绝对路径 - 过滤并忽略空字符串、非字符串及相对路径的 imagePaths 项 - 在单元测试中覆盖 imagePaths 参数支持及无效项过滤逻辑 - 在文档中补充 Qoder 运行时适配器对 --attachment 参数的说明 Change-Id: Ibfc3583ba86c6d258d524912559e97b77bf1dc87 Co-developed-by: Qoder <noreply@qoder.com> * docs(runtime): 说明Qoder适配器继承用户令牌的环境变量 - 添加文档说明检测代理仅为可用性探针,不进行身份验证 - 说明Qoder CLI账号状态独立,认证通过运行时错误路径反馈 - 详细描述子进程环境继承机制及静态环境变量与用户私密令牌区分 - 明确QODER_PERSONAL_ACCESS_TOKEN通过守护进程环境传递,不写入静态环境 - 解释Qoder验证由Qoder CLI负责,支持持久登录和自动化环境变量注入 test(agent): 添加QODER_PERSONAL_ACCESS_TOKEN环境变量继承测试 - 验证qoder适配器环境继承守护进程中的QODER_PERSONAL_ACCESS_TOKEN - 确认qoder适配器未在静态环境变量中定义用户令牌 - 保证用户私密令牌不会被写入静态适配器环境配置 Change-Id: Ie61869afbe497df1b16879b4e47b35123f758ed8 Co-developed-by: Qoder <noreply@qoder.com> * fix(daemon): 改进Qoder模式支持及错误处理机制 - 更新Qoder CLI参数,使用`--yolo`替代`--permission-mode bypass_permissions` - 将工作目录参数从`--cwd`改为`-w`以符合Qoder文档 - 在agent流事件处理中新增错误捕获并通过SSE错误事件发送 - 运行结束时若检测到agent流错误,则标记运行失败 - 测试中fix(daemon): 优化Qoder代理参数与错误处理 - 调整Qoder启动参数,改用`--yolo`和`-w`替代旧参数,避开argv长度限制 - 增强代理流事件处理,捕获并通过SSE错误通同步更新Qoder参数使用及相应断言 - 新增端到端测试,覆盖Qoder助手错误通过SSE错误通道反馈及运行状态失败处理 - 补充工具函数辅助测试事件流读取与运行状态轮询 Change-Id: I5d933745c3659e093b0d2d807f22726e7f83eb48 Co-developed-by: Qoder <noreply@qoder.com> * feat(qoder-stream): 识别并报告Qoder运行错误事件 - 新增messageFromResult函数以从结果对象提取错误信息 - 在处理result事件时根据is_error字段触发error事件 - error事件携带具体错误消息和原始数据 - 添加测试验证Qoder运行返回is_error且退出码为0时正确触发错误事件 - 更新qoder流解析测试以校验错误事件映射 - 在聊天路由测试中增加针对Qoder错误运行的端到端场景验证 Change-Id: Ie98ac518135dbec3181c52de5a49afdea993e279 Co-developed-by: Qoder <noreply@qoder.com>
228 lines
5.1 KiB
TypeScript
228 lines
5.1 KiB
TypeScript
// @ts-nocheck
|
|
import { test } from 'vitest';
|
|
import assert from 'node:assert/strict';
|
|
import { createQoderStreamHandler } from '../src/qoder-stream.js';
|
|
|
|
function parseLines(lines) {
|
|
const events = [];
|
|
const handler = createQoderStreamHandler((event) => events.push(event));
|
|
for (const line of lines) {
|
|
handler.feed(`${line}\n`);
|
|
}
|
|
handler.flush();
|
|
return events;
|
|
}
|
|
|
|
test('qoder stream parser maps system init to status', () => {
|
|
const events = parseLines([
|
|
JSON.stringify({
|
|
type: 'system',
|
|
subtype: 'init',
|
|
qodercli_version: '0.2.6',
|
|
model: 'auto',
|
|
session_id: 'session-1',
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
type: 'status',
|
|
label: 'initializing',
|
|
model: 'auto',
|
|
sessionId: 'session-1',
|
|
qodercliVersion: '0.2.6',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser maps assistant text content blocks to text deltas', () => {
|
|
const events = parseLines([
|
|
JSON.stringify({
|
|
type: 'assistant',
|
|
message: {
|
|
content: [
|
|
{ type: 'text', text: 'Hello' },
|
|
{ type: 'text', text: ' world' },
|
|
],
|
|
},
|
|
session_id: 'session-1',
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(events, [
|
|
{ type: 'text_delta', delta: 'Hello' },
|
|
{ type: 'text_delta', delta: ' world' },
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser maps assistant errors without text to error events', () => {
|
|
const line = JSON.stringify({
|
|
type: 'assistant',
|
|
message: { content: [] },
|
|
error: { message: 'Qoder authentication expired' },
|
|
});
|
|
const events = parseLines([line]);
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
type: 'error',
|
|
message: 'Qoder authentication expired',
|
|
raw: line,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser uses a fallback message for assistant errors without detail', () => {
|
|
const line = JSON.stringify({
|
|
type: 'assistant',
|
|
message: { content: [] },
|
|
error: { code: 'E_QODER' },
|
|
});
|
|
const events = parseLines([line]);
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
type: 'error',
|
|
message: 'Unknown Qoder error',
|
|
raw: line,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser preserves text from assistant records that also include errors', () => {
|
|
const events = parseLines([
|
|
JSON.stringify({
|
|
type: 'assistant',
|
|
message: {
|
|
content: [{ type: 'text', text: 'Partial answer' }],
|
|
},
|
|
error: { message: 'Trailing Qoder warning' },
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(events, [{ type: 'text_delta', delta: 'Partial answer' }]);
|
|
});
|
|
|
|
test('qoder stream parser maps thinking content blocks to thinking events', () => {
|
|
const events = parseLines([
|
|
JSON.stringify({
|
|
type: 'assistant',
|
|
message: {
|
|
content: [
|
|
{
|
|
type: 'thinking',
|
|
thinking: 'Considering the exact response.',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(events, [
|
|
{ type: 'thinking_start' },
|
|
{
|
|
type: 'thinking_delta',
|
|
delta: 'Considering the exact response.',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser maps result usage and preserves modelUsage', () => {
|
|
const usage = {
|
|
input_tokens: 10,
|
|
output_tokens: 2,
|
|
service_tier: 'standard',
|
|
};
|
|
const modelUsage = {
|
|
auto: {
|
|
inputTokens: 10,
|
|
outputTokens: 2,
|
|
costUSD: 0,
|
|
},
|
|
};
|
|
const events = parseLines([
|
|
JSON.stringify({
|
|
type: 'result',
|
|
subtype: 'success',
|
|
duration_ms: 10864,
|
|
is_error: false,
|
|
stop_reason: 'end_turn',
|
|
total_cost_usd: 0,
|
|
usage,
|
|
modelUsage,
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
type: 'usage',
|
|
usage,
|
|
modelUsage,
|
|
costUsd: 0,
|
|
durationMs: 10864,
|
|
stopReason: 'end_turn',
|
|
isError: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser maps result is_error to a fatal error event', () => {
|
|
const usage = {
|
|
input_tokens: 10,
|
|
output_tokens: 2,
|
|
};
|
|
const line = JSON.stringify({
|
|
type: 'result',
|
|
subtype: 'error',
|
|
duration_ms: 10864,
|
|
is_error: true,
|
|
stop_reason: 'tool_use_failed',
|
|
total_cost_usd: 0,
|
|
usage,
|
|
});
|
|
const events = parseLines([line]);
|
|
|
|
assert.deepEqual(events, [
|
|
{
|
|
type: 'usage',
|
|
usage,
|
|
modelUsage: undefined,
|
|
costUsd: 0,
|
|
durationMs: 10864,
|
|
stopReason: 'tool_use_failed',
|
|
isError: true,
|
|
},
|
|
{
|
|
type: 'error',
|
|
message: 'Qoder run failed: tool_use_failed',
|
|
raw: line,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser forwards unknown and malformed lines as raw events', () => {
|
|
const events = parseLines([
|
|
'{"type":"unknown","value":1}',
|
|
'not json',
|
|
]);
|
|
|
|
assert.deepEqual(events, [
|
|
{ type: 'raw', line: '{"type":"unknown","value":1}' },
|
|
{ type: 'raw', line: 'not json' },
|
|
]);
|
|
});
|
|
|
|
test('qoder stream parser flushes a trailing line without newline', () => {
|
|
const events = [];
|
|
const handler = createQoderStreamHandler((event) => events.push(event));
|
|
handler.feed(
|
|
JSON.stringify({
|
|
type: 'assistant',
|
|
message: { content: [{ type: 'text', text: 'OK' }] },
|
|
}),
|
|
);
|
|
handler.flush();
|
|
|
|
assert.deepEqual(events, [{ type: 'text_delta', delta: 'OK' }]);
|
|
});
|