Agent 的 `src/` 目录下的 TypeScript 文件会被自动编译并注册。每个导出的函数都会成为一个 **Yao Process**——可按名称从 Hook、MCP 工具、其他脚本调用,也可通过 `yao run` 直接执行。
## 命名规则
Process 名称从文件路径推导:
```
assistants/<namespace>/<agent-id>/src/<filename>.ts
↓ ↓ ↓
agents.<namespace>.<agent-id>.<filename>.<FunctionName>
```
**示例**(Agent 位于 `assistants/myns/myagent/`):
| 文件 | 导出函数 | Process 名称 |
|------|---------|-------------|
| `src/logic.ts` | `Save` | `agents.myns.myagent.logic.Save` |
| `src/logic.ts` | `Search` | `agents.myns.myagent.logic.Search` |
> **注意:** `src/index.ts` 比较特殊 —— 它导出的 `Create` 和 `Next` 会作为 **Hook 函数** 加载,而不会注册为 Process。只有非 index 脚本才会成为 Process。
子目录文件遵循相同规则:
| 文件 | Process 前缀 |
|------|-------------|
| `src/logic/core.ts` | `agents.myns.myagent.logic.core.<Fn>` |
| `src/utils/format.ts` | `agents.myns.myagent.utils.format.<Fn>` |
> **注意:** 不打算对外调用的内部辅助函数不应导出——只有顶层文件才应导出公开 API。
## Runtime API
从 `@yao/runtime` 导入:
```typescript
import {
Process, // 按名称调用任意 Yao Process
Exception, // 抛出结构化错误
Authorized, // 获取当前用户/团队上下文
log, // 结构化日志
Store, // 键值存储(跨请求共享)
FS, // 文件系统访问
Query, // 原始 SQL 查询
} from "@yao/runtime";
```
### `Process(name, ...args)`
调用任意 Yao Process——模型、其他脚本、内置 Process:
```typescript
// 查询 Agent 的 Model
const MODEL = "agents.myns.myagent";
const records = Process(`models.${MODEL}.record.Get`, {
wheres: [{ column: "team_id", value: teamId }],
limit: 20
});
// 调用同一 Agent 的另一个脚本
const result = Process("agents.myns.myagent.logic.Save", teamId, data);
// 调用内置 Process
const html = Process("utils.http.Get", "https://example.com");
```
### `Authorized()`
返回当前用户和团队上下文。在 MCP 适配函数中调用,可在不向 LLM 暴露 `team_id` 的情况下注入它:
```typescript
import { Authorized, Exception } from "@yao/runtime";
function getTeamId(): string {
const auth = Authorized();
if (auth?.team_id) return auth.team_id;
throw new Exception("team_id required", 400);
}
```
### `Exception(message, code)`
抛出结构化错误。`code` 是返回给调用方的 HTTP 状态码:
```typescript
throw new Exception("title is required", 400);
throw new Exception("entry not found", 404);
throw new Exception("internal error", 500);
```
### `log`
```typescript
log.Trace("processing record", { id });
log.Info("record saved", { id, title });
log.Warn("field missing, skipping", { field });
log.Error("unexpected error", { error: e.message });
```
## 两层模式
最常见的结构是两层架构:
**第一层——内部辅助函数**(`src/logic/core.ts`,不作为 Process 导出):
```typescript
export function saveRecord(teamId: string, data: RecordData): Result {
const MODEL = "agents.myns.myagent";
return Process(`models.${MODEL}.record.Save`, { team_id: teamId, ...data });
}
```
**第二层——公开 API + MCP 适配器**(`src/logic.ts`,导出为 Process):
```typescript
// 注册为 agents.myns.myagent.logic.*
import { saveRecord } from "./logic/core";
import { Authorized, Exception } from "@yao/runtime";
// Process — 可传入显式 teamId(如从 Hook 调用)
export function Save(teamId: string, data: RecordData): Result {
if (!data?.title) throw new Exception("title is required", 400);
return saveRecord(teamId, data);
}
// MCP 适配器 — 由 LLM 调用(参数中不含 teamId)
export function MCPSave(params: { title: string; content: string }): Result {
if (!params.title) throw new Exception("title is required", 400);
const auth = Authorized();
if (!auth?.team_id) throw new Exception("unauthorized", 401);
return Save(auth.team_id, params);
}
```
`team_id` 通过 `Authorized()` 在服务端解析——LLM 从不感知或提供它。
## 在 Hook 中调用脚本
```typescript
// src/index.ts
import { Process } from "@yao/runtime";
export function Next(ctx, payload) {
const content = payload.completion?.content as string;
const result = Process(
"agents.myns.myagent.logic.Save",
ctx.authorized.team_id,
{ title: "...", content }
);
ctx.Send({ type: "text", props: { content: `已保存:${result.id}` } });
return null;
}
```
## 单元测试
脚本支持内置单元测试。测试文件命名为 `<name>_test.ts`,放在源文件旁边,导出函数名以 `Test` 开头,用 `yao agent test` 运行。
### 测试文件
```typescript
// src/logic_test.ts
// @ts-nocheck
import { testing, agent } from "@yao/runtime";
import { Save, Search } from "./logic";
export function TestSave(t: testing.T, ctx: agent.Context) {
const teamId = ctx.authorized?.team_id || "test-team";
const result = Save(teamId, { title: "Hello", content: "World" });
t.assert.True(result.id > 0, "id 应被赋值");
t.assert.Equal(result.title, "Hello", "title 应匹配");
}
export function TestSave_MissingTitle(t: testing.T, ctx: agent.Context) {
let threw = false;
try {
Save("test-team", { title: "" } as any);
} catch (e: any) {
threw = true;
t.assert.Contains(e.message, "title is required");
}
t.assert.True(threw, "title 为空时应抛出异常");
}
export function TestSearch(t: testing.T, ctx: agent.Context) {
const teamId = ctx.authorized?.team_id || "test-team";
const results = Search(teamId, { limit: 5 });
t.assert.True(Array.isArray(results), "应返回数组");
}
```
### 上下文文件
提供测试上下文 JSON,使 `ctx.authorized` 有值:
```json
// tests/ctx.json
{
"chat_id": "test-chat",
"authorized": {
"user_id": "test-user",
"team_id": "test-team"
}
}
```
### 运行测试
```bash
# 运行脚本文件中的所有测试
yao agent test -i scripts.myns.myagent.logic \
--ctx ./assistants/myns/myagent/tests/ctx.json -v
# 运行单个测试函数(--run 接受正则表达式)
yao agent test -i scripts.myns.myagent.logic \
--ctx ./assistants/myns/myagent/tests/ctx.json -v --run TestSave
```
`-i` 参数使用 `scripts.<assistant-path>.<module>` 格式(不是 Process 名称或文件路径)。`-v` 开启详细输出。`--run` 接受正则表达式过滤测试函数。
### 断言方法
| 方法 | 说明 |
|------|------|
| `t.assert.True(v, msg?)` | 断言值为真 |
| `t.assert.False(v, msg?)` | 断言值为假 |
| `t.assert.Equal(actual, expected, msg?)` | 深度相等 |
| `t.assert.NotEqual(actual, expected, msg?)` | 不相等 |
| `t.assert.Nil(v, msg?)` | 断言为 null 或 undefined |
| `t.assert.NotNil(v, msg?)` | 断言不为 null/undefined |
| `t.assert.Contains(str, substr, msg?)` | 字符串包含 |
| `t.assert.Len(v, n, msg?)` | 数组/字符串长度 |
| `t.assert.Greater(a, b, msg?)` | `a > b` |
| `t.assert.Match(v, pattern, msg?)` | 正则匹配 |
| `t.log(msg)` | 打印日志行 |
| `t.skip(reason?)` | 跳过此测试 |
| `t.fail(reason?)` | 标记测试失败(继续运行) |
| `t.fatal(reason?)` | 标记测试失败并立即停止 |
## 下一步
脚本就绪后,将其暴露给 LLM 作为 MCP 工具:
- **[MCP 工具(Yao Process)→](./mcp)**