@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.0
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/CONTRACT.md +276 -0
- package/LICENSE +21 -0
- package/README.md +299 -2
- package/build.mjs +89 -0
- package/client.js +7871 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +26 -0
- package/index.js +390 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +66 -4
- package/preview/dataflow.png +0 -0
- package/preview/inspector-context.png +0 -0
- package/preview/model-switch.png +0 -0
- package/preview/overview-dark.png +0 -0
- package/preview/overview-light.png +0 -0
- package/preview/principles.png +0 -0
- package/preview/source.png +0 -0
- package/src/00-core.js +209 -0
- package/src/10-engine.js +1960 -0
- package/src/20-tools.js +600 -0
- package/src/30-llm.js +495 -0
- package/src/40-ui-kit.js +596 -0
- package/src/45-real-client.js +190 -0
- package/src/50-tui.js +472 -0
- package/src/60-inspector.js +621 -0
- package/src/65-dataflow.js +341 -0
- package/src/70-docs.js +1226 -0
- package/src/80-source.js +576 -0
- package/src/90-page.js +476 -0
- package/src/95-apply.js +58 -0
- package/test/engine.test.mjs +800 -0
- package/test/extra-tools.test.mjs +168 -0
- package/test/real-mode.test.mjs +177 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test/extra-tools.test.mjs · 新增趣味工具与规则模型意图测试
|
|
3
|
+
* 覆盖 dice_roll / random_pick / password_generate / unit_convert,
|
|
4
|
+
* 以及模拟模型能否正确选择并回答这些工具。
|
|
5
|
+
* 运行:node test/extra-tools.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import vm from 'node:vm';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const srcDir = path.join(here, '..', 'src');
|
|
14
|
+
const files = ['00-core.js', '10-engine.js', '20-tools.js', '30-llm.js'];
|
|
15
|
+
const bundle =
|
|
16
|
+
files.map((file) => fs.readFileSync(path.join(srcDir, file), 'utf8')).join('\n\n') +
|
|
17
|
+
'\n;globalThis.__LAB__ = { createLab: createLab, LAB_TOOLS: LAB_TOOLS, LAB_UNIT_ALIASES: LAB_UNIT_ALIASES };\n';
|
|
18
|
+
const sandbox = { console, setTimeout, clearTimeout };
|
|
19
|
+
vm.createContext(sandbox);
|
|
20
|
+
new vm.Script(bundle, { filename: 'agent-lab.tools.vm.js' }).runInContext(sandbox);
|
|
21
|
+
const { createLab, LAB_TOOLS } = sandbox.__LAB__;
|
|
22
|
+
|
|
23
|
+
let passCount = 0;
|
|
24
|
+
let failCount = 0;
|
|
25
|
+
const failures = [];
|
|
26
|
+
function ok(condition, message) {
|
|
27
|
+
if (!condition) throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
function eq(actual, expected, message) {
|
|
30
|
+
if (actual !== expected) throw new Error(message + ':期望 ' + JSON.stringify(expected) + ',实际 ' + JSON.stringify(actual));
|
|
31
|
+
}
|
|
32
|
+
function includes(haystack, needle, message) {
|
|
33
|
+
if (String(haystack).indexOf(needle) < 0) throw new Error(message + ':未包含 ' + JSON.stringify(needle) + '(实际:' + String(haystack).slice(0, 240) + ')');
|
|
34
|
+
}
|
|
35
|
+
async function test(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
await fn();
|
|
38
|
+
passCount += 1;
|
|
39
|
+
console.log('PASS ' + name);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
failCount += 1;
|
|
42
|
+
failures.push({ name, message: error.message });
|
|
43
|
+
console.log('FAIL ' + name + ' · ' + error.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function tool(name) {
|
|
47
|
+
return LAB_TOOLS.find((item) => item.name === name);
|
|
48
|
+
}
|
|
49
|
+
function makeLab() {
|
|
50
|
+
const data = new Map();
|
|
51
|
+
return createLab({
|
|
52
|
+
sleep: async () => {},
|
|
53
|
+
pace: 'fast',
|
|
54
|
+
storage: {
|
|
55
|
+
getItem: (key) => (data.has(key) ? data.get(key) : null),
|
|
56
|
+
setItem: (key, value) => data.set(key, String(value)),
|
|
57
|
+
removeItem: (key) => data.delete(key),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function transcript(snapshot) {
|
|
62
|
+
return snapshot.transcript.map((line) => line.text).join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await test('工具注册表:8 个工具且 schema 完整', async () => {
|
|
66
|
+
eq(LAB_TOOLS.length, 8, '工具数量');
|
|
67
|
+
for (const name of ['calculator', 'clock', 'dice_roll', 'random_pick', 'password_generate', 'unit_convert', 'memory_write', 'memory_search']) {
|
|
68
|
+
const item = tool(name);
|
|
69
|
+
ok(item, '缺少工具 ' + name);
|
|
70
|
+
ok(item.parameters && item.parameters.type === 'object', name + ' 参数 schema');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await test('dice_roll:面数 / 个数 / 范围', async () => {
|
|
75
|
+
const result = tool('dice_roll').run({ sides: 6, count: 2 }, {});
|
|
76
|
+
ok(result.ok, '应成功');
|
|
77
|
+
eq(result.result.rolls.length, 2, '掷两个');
|
|
78
|
+
for (const value of result.result.rolls) ok(value >= 1 && value <= 6, '点数范围');
|
|
79
|
+
eq(result.result.total, result.result.rolls[0] + result.result.rolls[1], '合计');
|
|
80
|
+
const dnd = tool('dice_roll').run({ sides: 20, count: 3 }, {});
|
|
81
|
+
ok(dnd.result.rolls.every((n) => n >= 1 && n <= 20), 'D20 范围');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await test('random_pick:从候选项中选一个', async () => {
|
|
85
|
+
const items = ['火锅', '烧烤', '日料'];
|
|
86
|
+
const result = tool('random_pick').run({ items }, {});
|
|
87
|
+
ok(result.ok, '应成功');
|
|
88
|
+
ok(items.indexOf(result.result.choice) >= 0, '结果必须在候选中');
|
|
89
|
+
eq(tool('random_pick').run({ items: ['只有一个'] }, {}).ok, false, '少于两个应失败');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('password_generate:长度与字符集', async () => {
|
|
93
|
+
const result = tool('password_generate').run({ length: 20, numbers: true, symbols: true }, {});
|
|
94
|
+
ok(result.ok, '应成功');
|
|
95
|
+
eq(result.result.password.length, 20, '长度');
|
|
96
|
+
ok(/[a-z]/.test(result.result.password), '包含小写');
|
|
97
|
+
ok(/[A-Z]/.test(result.result.password), '包含大写');
|
|
98
|
+
ok(/[0-9]/.test(result.result.password), '包含数字');
|
|
99
|
+
ok(/[!@#$%^&*_=+?]/.test(result.result.password), '包含符号');
|
|
100
|
+
eq(tool('password_generate').run({ length: 4 }, {}).result.length, 6, '长度下限修正为 6');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('unit_convert:长度 / 温度 / 重量', async () => {
|
|
104
|
+
const km = tool('unit_convert').run({ value: 100, from: '公里', to: '英里' }, {});
|
|
105
|
+
ok(km.ok, '公里→英里应成功');
|
|
106
|
+
ok(Math.abs(km.result.converted - 62.137119) < 0.001, '100 公里 ≈ 62.137 英里,实际 ' + km.result.converted);
|
|
107
|
+
const temp = tool('unit_convert').run({ value: 0, from: '摄氏度', to: '华氏度' }, {});
|
|
108
|
+
eq(temp.result.converted, 32, '0°C = 32°F');
|
|
109
|
+
const mass = tool('unit_convert').run({ value: 1, from: 'kg', to: 'lb' }, {});
|
|
110
|
+
ok(Math.abs(mass.result.converted - 2.204623) < 0.001, '1kg ≈ 2.2046lb');
|
|
111
|
+
eq(tool('unit_convert').run({ value: 1, from: 'kg', to: 'km' }, {}).ok, false, '不同量纲应失败');
|
|
112
|
+
eq(tool('unit_convert').run({ value: 1, from: '斤', to: 'kg' }, {}).result.converted, 0.5, '1 斤 = 0.5kg');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await test('模拟模型:掷骰子意图与工具链', async () => {
|
|
116
|
+
const lab = makeLab();
|
|
117
|
+
await lab.submit('掷两个 20 面骰子');
|
|
118
|
+
const snap = lab.snapshot();
|
|
119
|
+
const text = transcript(snap);
|
|
120
|
+
includes(text, 'dice_roll', '应调用 dice_roll');
|
|
121
|
+
includes(text, '🎲', '回答应带骰子结果');
|
|
122
|
+
eq(snap.turn.status, 'done', 'turn 完成');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('模拟模型:随机选择意图', async () => {
|
|
126
|
+
const lab = makeLab();
|
|
127
|
+
await lab.submit('从 火锅、烧烤、日料 里帮我选一个');
|
|
128
|
+
const snap = lab.snapshot();
|
|
129
|
+
includes(transcript(snap), 'random_pick', '应调用 random_pick');
|
|
130
|
+
includes(transcript(snap), '我选:', '回答应给出选择');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('模拟模型:生成密码意图', async () => {
|
|
134
|
+
const lab = makeLab();
|
|
135
|
+
await lab.submit('生成一个 20 位密码');
|
|
136
|
+
const snap = lab.snapshot();
|
|
137
|
+
includes(transcript(snap), 'password_generate', '应调用 password_generate');
|
|
138
|
+
includes(transcript(snap), '20 位', '回答应说明长度');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await test('模拟模型:单位换算意图', async () => {
|
|
142
|
+
const lab = makeLab();
|
|
143
|
+
await lab.submit('100 公里等于多少英里');
|
|
144
|
+
const snap = lab.snapshot();
|
|
145
|
+
const text = transcript(snap);
|
|
146
|
+
includes(text, 'unit_convert', '应调用 unit_convert');
|
|
147
|
+
includes(text, '62.13', '换算结果应正确');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await test('模拟模型:工具结果可链式写入长期记忆', async () => {
|
|
151
|
+
const lab = makeLab();
|
|
152
|
+
await lab.submit('掷两个骰子,然后记住结果');
|
|
153
|
+
const snap = lab.snapshot();
|
|
154
|
+
const text = transcript(snap);
|
|
155
|
+
includes(text, 'dice_roll', '应调用 dice_roll');
|
|
156
|
+
includes(text, 'memory_write', '应链式调用 memory_write');
|
|
157
|
+
const entry = snap.memory.find((item) => item.key === '掷骰结果');
|
|
158
|
+
ok(entry && entry.value, '长期记忆应包含掷骰结果');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log('\n────────────────────────────────────────────────────────');
|
|
162
|
+
if (failCount === 0) {
|
|
163
|
+
console.log(passCount + ' PASS · 0 FAIL · 新增工具全部通过');
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
console.log(passCount + ' PASS · ' + failCount + ' FAIL');
|
|
167
|
+
for (const failure of failures) console.log(' ✗ ' + failure.name + ' · ' + failure.message);
|
|
168
|
+
process.exit(1);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test/real-mode.test.mjs · 真实模型模式无头测试
|
|
3
|
+
*
|
|
4
|
+
* 不联网:用注入的 modelAdapter 模拟 Host 的真实模型响应,验证
|
|
5
|
+
* - 引擎在 real 模式下走异步 adapter,而不是规则模型
|
|
6
|
+
* - 工具调用消息携带 toolCallId / toolArgs,并写回 tool 消息
|
|
7
|
+
* - 第二轮上下文顺序为 user → assistant(tool_call) → tool
|
|
8
|
+
* - 模型 stage 记录 provider/model/usage/latency,snapshot 暴露模式与用量
|
|
9
|
+
* - 未注入 adapter 时给出明确错误
|
|
10
|
+
* - 模式持久化与切回模拟
|
|
11
|
+
*
|
|
12
|
+
* 运行:node test/real-mode.test.mjs
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import vm from 'node:vm';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const srcDir = path.join(here, '..', 'src');
|
|
21
|
+
const files = ['00-core.js', '10-engine.js', '20-tools.js', '30-llm.js'];
|
|
22
|
+
const bundle =
|
|
23
|
+
files.map((file) => fs.readFileSync(path.join(srcDir, file), 'utf8')).join('\n\n') +
|
|
24
|
+
'\n;globalThis.__LAB__ = { createLab: createLab, LAB_STORAGE_KEY: LAB_STORAGE_KEY };\n';
|
|
25
|
+
|
|
26
|
+
const sandbox = { console, setTimeout, clearTimeout };
|
|
27
|
+
vm.createContext(sandbox);
|
|
28
|
+
new vm.Script(bundle, { filename: 'agent-lab.real.vm.js' }).runInContext(sandbox);
|
|
29
|
+
const { createLab } = sandbox.__LAB__;
|
|
30
|
+
|
|
31
|
+
let passCount = 0;
|
|
32
|
+
let failCount = 0;
|
|
33
|
+
const failures = [];
|
|
34
|
+
function ok(condition, message) {
|
|
35
|
+
if (!condition) throw new Error(message);
|
|
36
|
+
}
|
|
37
|
+
function eq(actual, expected, message) {
|
|
38
|
+
if (actual !== expected) {
|
|
39
|
+
throw new Error(message + ':期望 ' + JSON.stringify(expected) + ',实际 ' + JSON.stringify(actual));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function includes(haystack, needle, message) {
|
|
43
|
+
if (String(haystack).indexOf(needle) < 0) {
|
|
44
|
+
throw new Error(message + ':未包含 ' + JSON.stringify(needle) + '(实际:' + String(haystack).slice(0, 300) + ')');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function test(name, fn) {
|
|
48
|
+
try {
|
|
49
|
+
await fn();
|
|
50
|
+
passCount += 1;
|
|
51
|
+
console.log('PASS ' + name);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
failCount += 1;
|
|
54
|
+
failures.push({ name, message: error.message });
|
|
55
|
+
console.log('FAIL ' + name + ' · ' + error.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function mockStorage() {
|
|
59
|
+
const data = new Map();
|
|
60
|
+
return {
|
|
61
|
+
getItem(key) {
|
|
62
|
+
return data.has(key) ? data.get(key) : null;
|
|
63
|
+
},
|
|
64
|
+
setItem(key, value) {
|
|
65
|
+
data.set(key, String(value));
|
|
66
|
+
},
|
|
67
|
+
removeItem(key) {
|
|
68
|
+
data.delete(key);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const USAGE = { inputTokens: 320, outputTokens: 24, totalTokens: 344 };
|
|
74
|
+
|
|
75
|
+
await test('真实模式:异步 adapter + 工具调用 + 回填结果', async () => {
|
|
76
|
+
const lab = createLab({ sleep: async () => {}, storage: mockStorage() });
|
|
77
|
+
const calls = [];
|
|
78
|
+
lab.setModelAdapter(async (request) => {
|
|
79
|
+
calls.push(request);
|
|
80
|
+
if (calls.length === 1) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'tool_call',
|
|
83
|
+
name: 'calculator',
|
|
84
|
+
args: { expression: '(12+30)*3' },
|
|
85
|
+
provider: 'deepseek-account',
|
|
86
|
+
model: 'deepseek-flash',
|
|
87
|
+
latencyMs: 800,
|
|
88
|
+
usage: USAGE,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
type: 'final',
|
|
93
|
+
text: '(12+30)*3 = 126',
|
|
94
|
+
provider: 'deepseek-account',
|
|
95
|
+
model: 'deepseek-flash',
|
|
96
|
+
latencyMs: 600,
|
|
97
|
+
usage: USAGE,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
eq(lab.setModelMode('real'), true, '切换到真实模式');
|
|
101
|
+
await lab.submit('算一下 (12+30)*3');
|
|
102
|
+
const s = lab.snapshot();
|
|
103
|
+
eq(calls.length, 2, '真实模型被调用两次');
|
|
104
|
+
eq(s.modelMode, 'real', 'snapshot.modelMode');
|
|
105
|
+
eq(s.model.provider, 'deepseek-account', 'snapshot.model.provider');
|
|
106
|
+
eq(s.model.lastUsage.inputTokens, 320, 'snapshot.model.lastUsage');
|
|
107
|
+
const text = s.transcript.map((line) => line.text).join('\n');
|
|
108
|
+
includes(text, '真实模型 · deepseek-flash · 800 ms', 'transcript 记录真实模型调用');
|
|
109
|
+
includes(text, '126', '工具结果或最终回答包含 126');
|
|
110
|
+
const assistantCall = s.messages.find((message) => message.kind === 'tool_call');
|
|
111
|
+
ok(assistantCall && assistantCall.toolCallId, 'assistant tool_call 带 toolCallId');
|
|
112
|
+
const toolResult = s.messages.find((message) => message.role === 'tool');
|
|
113
|
+
ok(toolResult && toolResult.toolCallId === assistantCall.toolCallId, 'tool 消息复用同一个 toolCallId');
|
|
114
|
+
const modelStage = s.turn.stages.find((stage) => stage.kind === 'model');
|
|
115
|
+
ok(modelStage.detail.model && modelStage.detail.model.model === 'deepseek-flash', 'model stage 记录模型信息');
|
|
116
|
+
/* 第二轮上下文顺序:user → assistant(tool_call) → tool */
|
|
117
|
+
const second = calls[1].messages;
|
|
118
|
+
const userIndex = second.findIndex((message) => message.role === 'user');
|
|
119
|
+
const callIndex = second.findIndex((message) => message.kind === 'tool_call');
|
|
120
|
+
const toolIndex = second.findIndex((message) => message.role === 'tool');
|
|
121
|
+
ok(userIndex >= 0 && callIndex > userIndex && toolIndex > callIndex, '第二轮消息顺序 user → assistant(tool_call) → tool');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('真实模式:未注入 adapter 时明确报错', async () => {
|
|
125
|
+
const lab = createLab({ sleep: async () => {}, storage: mockStorage() });
|
|
126
|
+
lab.setModelMode('real');
|
|
127
|
+
await lab.submit('你好');
|
|
128
|
+
const s = lab.snapshot();
|
|
129
|
+
const text = s.transcript.map((line) => line.text).join('\n');
|
|
130
|
+
includes(text, '真实模型适配器未连接', '错误提示');
|
|
131
|
+
eq(s.turn.status, 'error', 'turn 状态');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await test('真实模式:模式可持久化,切回模拟后走规则模型', async () => {
|
|
135
|
+
const storage = mockStorage();
|
|
136
|
+
const first = createLab({ sleep: async () => {}, storage });
|
|
137
|
+
first.setModelMode('real');
|
|
138
|
+
first.dispose();
|
|
139
|
+
const second = createLab({ sleep: async () => {}, storage });
|
|
140
|
+
eq(second.snapshot().modelMode, 'real', '重建后仍是真实模式');
|
|
141
|
+
let realCalls = 0;
|
|
142
|
+
second.setModelAdapter(async () => {
|
|
143
|
+
realCalls += 1;
|
|
144
|
+
return { type: 'final', text: '真实回答', provider: 'deepseek-account', model: 'deepseek-flash', latencyMs: 10, usage: USAGE };
|
|
145
|
+
});
|
|
146
|
+
await second.submit('你好');
|
|
147
|
+
eq(realCalls, 1, '真实 adapter 被调用');
|
|
148
|
+
second.setModelMode('sim');
|
|
149
|
+
await second.submit('帮助');
|
|
150
|
+
eq(realCalls, 1, '切回模拟后不再调用真实 adapter');
|
|
151
|
+
const text = second.snapshot().transcript.map((line) => line.text).join('\n');
|
|
152
|
+
includes(text, '我是教学用的迷你 Agent', '模拟模型回答');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await test('真实模式:调用失败转为可读错误,不抛出到 UI', async () => {
|
|
156
|
+
const lab = createLab({ sleep: async () => {}, storage: mockStorage() });
|
|
157
|
+
lab.setModelMode('real');
|
|
158
|
+
lab.setModelAdapter(async () => {
|
|
159
|
+
throw new Error('DeepSeek 账号额度不足');
|
|
160
|
+
});
|
|
161
|
+
await lab.submit('帮我算一下 1+1');
|
|
162
|
+
const s = lab.snapshot();
|
|
163
|
+
const text = s.transcript.map((line) => line.text).join('\n');
|
|
164
|
+
includes(text, 'DeepSeek 账号额度不足', '错误文本进入 transcript');
|
|
165
|
+
eq(s.turn.status, 'error', 'turn 错误状态');
|
|
166
|
+
eq(s.busy, false, '失败后回到空闲');
|
|
167
|
+
eq(s.model.lastError, 'DeepSeek 账号额度不足', 'snapshot.model.lastError');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
console.log('\n────────────────────────────────────────────────────────');
|
|
171
|
+
if (failCount === 0) {
|
|
172
|
+
console.log(passCount + ' PASS · 0 FAIL · 真实模式全部通过');
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
console.log(passCount + ' PASS · ' + failCount + ' FAIL');
|
|
176
|
+
for (const failure of failures) console.log(' ✗ ' + failure.name + ' · ' + failure.message);
|
|
177
|
+
process.exit(1);
|