Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions example/parsar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Open http://127.0.0.1:18180. `OPENAI_BASE_URL` ends in `/v1`; remote endpoints r
| **资源库** | Building blocks, grouped by what uses them: models for Agent engines; Skills, MCP servers, Plugins, knowledge and instruction fragments for Agents; credentials for MCP. Resources are shared: editing one affects everything that uses it. Only live configuration blocks removal: an Agent that binds an item, an Agent that uses a template or model, or a bot bound to an Agent. Conversations do not; they keep the configuration snapshot they were created with and their full history. |
| **环境模板** | The prepared workspace: npm and Python packages, a Bash setup script, network access (allowed, blocked or limited to listed domains), environment variables, the secrets it needs and initial files. |
| **Agent** | What people talk to: name, description, instructions, how it runs, one required environment template, capabilities and behavior switches. How it runs mirrors a local Codex configuration: Harness, model, reasoning effort, web search and, under **高级**, reasoning summary, verbosity and service tier. **复制** creates an independent copy. |
| **飞书机器人** | One bot is bound to at most one Agent. It routes its chats to that Agent and is the Agent’s Feishu identity in every conversation, including Web chats. |
| **飞书机器人** | One bot is bound to at most one Agent. It routes its chats to that Agent and is the Agent’s Feishu identity in every conversation, including Web chats. An Agent whose template or capabilities need the bot identity saves without one, but its conversations start only once a bot is bound. |
| **对话** | One public Core Session with its own Environment, created from the Agent's saved Core Agent and its environment template's Core Environment Template. Core copies both when the Session starts. |

Configuration lives only in templates and Agents; a conversation never overrides it. Edits affect only new conversations. When the Agent or its environment template changes, an existing conversation says so and offers **用新配置开始新对话**.
Expand Down Expand Up @@ -117,7 +117,7 @@ Optional business questions use the declared `ask_user` function. They appear in

**提前准备,新对话无需等待安装** keeps one prepared Session for each Agent that uses the template. Parsar creates it without input, so Core prepares its Environment ahead of time, and the next Web or Feishu conversation takes it over with its first message; the conversation keeps its own title. A prepared Session is replaced when the Agent, its environment template, bot or model provider changes, after 12 hours, or once taken; turning the switch off discards it. A first message with images or files starts an ordinary Session. Each prepared Session holds a workspace while it waits.

**试运行** creates one real conversation with an Agent that uses the template, waits for its first reply, shows success with the time or the plain-language failure, and deletes the conversation.
**试运行** creates one real conversation with an Agent that uses the template, waits for its first reply, shows success with the time or the plain-language failure, and deletes the conversation. Editing the template clears the result.

## Feishu

Expand All @@ -129,7 +129,7 @@ Optional business questions use the declared `ask_user` function. They appear in
4. Text, rich posts, images, files, audio and video, quoted messages and merged forwards are normalized before dispatch. Attachment limits are eight resources and 50 MiB total; files reach the workspace through Core's Files API. Follow-up files wait until the conversation is idle. Quoted messages are read up to four levels; earlier group messages are not read.
5. Commands: `/clear` clears the context: it stops any running work and the next message starts a new conversation, in a private chat or in the same group topic. `/stop` stops the current task; it takes priority over queued follow-ups and recovers an uncertain creation before stopping it. `/answer <call_id> <text>` answers a pending business question, and a plain reply answers when only one is pending. `/help` lists the commands.

Accepted messages are committed to SQLite before the WebSocket ACK; request IDs and frozen payloads survive retries and restarts. The worker edits one compact progress card in place, includes any pending business question in that card, and finishes it with the result. Each reply shows only the current exchange, including answers that resume the same Core Turn. The answer stays visible; while awaiting a business decision, the question takes priority and its supporting context is collapsed, except QR authorization instructions. The card has a colored header for its status. Its process is one timeline in the order things happened. Only commentary stays open as plain text; thinking folds behind its first sentence, and consecutive tool calls fold into one row (such as `运行 3 条命令` with their total time, failures or 进行中) in which each call folds again behind its command, showing parameters, output, error and exit code. While running, the header also says what is happening now and how many steps have run. A running card shows the latest four steps and folds the earlier ones into a tinted block above a divider; a finished card puts the answer first and folds the whole process into a tinted **处理过程** block; a failed card shows the failed step right under its cause. Long content is cut to marked, byte-bounded excerpts. Every step stays in the card: to stay within Feishu's card size limit, the oldest steps first drop their tool output, then shrink to one line, and only a turn with hundreds of steps summarizes its oldest ones in one line. Feishu messages never send people to the Web. Progress uses five-second polling. Complete history and Artifacts remain in the Web conversation. Settled threads stop polling after five minutes and wake on new messages. **消息记录** lists the latest 100 deliveries plus unresolved ones; **重试** keeps the original identity. A failed inbound message blocks later messages in that thread. Uncertain outbound sends are not replayed beyond 55 minutes. Disabling a bot pauses delivery without cancelling Core work.
Accepted messages are committed to SQLite before the WebSocket ACK; request IDs and frozen payloads survive retries and restarts. The worker edits one compact progress card in place, includes any pending business question in that card, and finishes it with the result. Each reply shows only the current exchange, including answers that resume the same Core Turn. The answer stays visible; while awaiting a business decision, the question takes priority and its supporting context is collapsed, except QR authorization instructions. The card has a colored header for its status. Its process is one timeline in the order things happened. Only commentary stays open as plain text; thinking folds behind its first sentence, and consecutive tool calls fold into one row (such as `运行 3 条命令` with their total time, failures or 进行中) in which each call folds again behind its command, showing parameters, output, error and exit code. While running, the header also says what is happening now and how many steps have run. A running card shows the latest four steps and folds the earlier ones into a tinted block above a divider; a finished card puts the answer first and folds the whole process into a tinted **处理过程** block; a failed card shows the failed step right under its cause. Long content is cut to marked, byte-bounded excerpts. Every step stays in the card: to stay within Feishu's card size limit, the oldest steps first drop their tool output, then shrink to one line, and only a turn with hundreds of steps summarizes its oldest ones in one line. Feishu messages never send people to the Web. Progress uses five-second polling. Complete history and Artifacts remain in the Web conversation. Settled threads stop polling after five minutes and wake on new messages. **消息记录** lists the latest 100 deliveries plus unresolved ones; **重试** resends a failed reply with its original identity. A refused inbound message is answered with the reason and does not hold up later messages in that thread. Uncertain outbound sends are not replayed beyond 55 minutes. Disabling a bot pauses delivery without cancelling Core work.

### Bot identity

Expand Down
8 changes: 4 additions & 4 deletions example/parsar/README.zh.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: example/parsar/README.md
source_hash: 60d0bf3a46840a363113be51df079bc4e1fc19c9b4ca88d2af145a6bc4581a9a
source_hash: 2503cb7b7c812ea0ac75ead9de95c2de267217117338464b572c9d05fcc96071
---

# Parsar 工作空间
Expand Down Expand Up @@ -29,7 +29,7 @@ pnpm --filter @oac/parsar-example dev
| **资源库** | 组装用的零件,按使用方分组:模型供 Agent 的执行引擎使用;Skills、MCP 服务、Plugins、知识和指令片段供 Agent 使用;凭据供 MCP 使用。资源是共享的:修改一项会影响所有使用它的对象。只有仍然有效的配置会阻止删除:Agent 绑定了这项能力、Agent 使用这个环境模板或模型,或者机器人绑定了这个 Agent。对话不算:它保留创建时的配置快照和完整历史。 |
| **环境模板** | 准备好的工作区:npm 与 Python 软件包、Bash 初始化脚本、联网权限(允许、禁止或仅允许指定域名)、环境变量、需要的密钥和初始文件。 |
| **Agent** | 用户对话的对象:名称、描述、指令、运行方式、一个必选的环境模板、能力和行为开关。运行方式对应本地 Codex 的配置:Harness、模型、推理强度、网页搜索,以及 **高级** 中的推理摘要、输出详细度和服务层级。**复制**会生成一份独立副本。 |
| **飞书机器人** | 一个机器人最多绑定一个 Agent。它把自己的聊天路由给该 Agent,并在所有对话中(包括网页对话)作为该 Agent 的飞书身份。 |
| **飞书机器人** | 一个机器人最多绑定一个 Agent。它把自己的聊天路由给该 Agent,并在所有对话中(包括网页对话)作为该 Agent 的飞书身份。模板或能力需要机器人身份的 Agent 可以先保存,绑定机器人后才能开始对话。 |
| **对话** | 一个公开 Core Session,拥有独立的 Environment,由 Agent 对应的 Core Agent 和环境模板对应的 Core Environment Template 创建。Session 开始时,Core 会复制这两份配置。 |

配置只在模板和 Agent 中设定,对话不会覆盖。修改只影响新对话。Agent 或其环境模板变更后,已有对话会给出提示,并提供 **用新配置开始新对话**。
Expand Down Expand Up @@ -122,7 +122,7 @@ Parsar 会跟踪每一轮正在执行的对话,网页和飞书都一样。出

**提前准备,新对话无需等待安装** 会为使用该模板的每个 Agent 保留一个准备好的 Session。Parsar 在没有输入的情况下创建它,Core 会提前准备好 Environment;下一个网页或飞书对话发送第一条消息时直接接管它,并保留自己的标题。Agent、其环境模板、机器人或模型服务商变更后,或超过 12 小时、或被接管后,准备好的 Session 会被替换;关闭开关会丢弃它。第一条消息带图片或文件时,会创建普通的 Session。每个准备好的 Session 在等待期间会占用一个工作区。

**试运行** 会用使用该模板的 Agent 发起一次真实对话,等待首次回复,显示成功及耗时或通俗的失败原因,然后删除这次对话。
**试运行** 会用使用该模板的 Agent 发起一次真实对话,等待首次回复,显示成功及耗时或通俗的失败原因,然后删除这次对话。修改模板后结果会清除。

## 飞书 {#feishu}

Expand All @@ -134,7 +134,7 @@ Parsar 会跟踪每一轮正在执行的对话,网页和飞书都一样。出
4. 文本、富文本、图片、文件、音视频、引用消息和合并转发会在投递前规范化。附件上限为 8 个、合计 50 MiB,文件通过 Core 的 Files API 写入工作区。后续消息中的文件会等到对话空闲后写入。引用消息最多读取四层;不读取更早的群消息。
5. 命令:`/clear` 清空上下文:停止正在执行的任务,下一条消息开始新对话,私聊和群聊话题都适用(群聊仍在同一个话题里)。`/stop` 停止当前任务,优先于排队中的追问,并会先确认不确定的创建再停止。`/answer <call_id> <回答>` 回答待处理的业务问题;只有一个待回答的问题时,直接回复即可。`/help` 列出这些命令。

收到的消息在 WebSocket ACK 之前写入 SQLite;请求 ID 和冻结的负载在重试与重启后保持不变。工作器原地更新一张简洁进度卡片,将待回答的问题合并到卡片中,结束后改成结果。每次回复只展示当前交流的内容,包括回答提问后继续同一 Core Turn 的情况。正文突出当前回复;等待业务选择时优先展示问题,将补充说明折叠,二维码授权说明保持可见。卡片顶部是带颜色的状态标题栏。处理过程按发生顺序排成一条时间线。只有说明直接显示文字;思考折叠在第一句后面;连续的工具调用折叠成一行(例如“运行 3 条命令”,附总用时、失败次数或“进行中”),展开后每次调用再折叠在它的命令后面,可查看参数、输出、错误和退出码。执行中时,标题栏还会显示当前在做什么和已执行的步数;卡片显示最近四步,更早的步骤折叠进分隔线上方的浅色区块;完成后回复放在最前面,整个过程折叠进浅色的 **处理过程** 区块;失败时出错的那一步紧跟在失败原因下面。长内容按字节限制截取并明确标注。每一步都留在卡片里:为了不超过飞书卡片的大小限制,最早的步骤先去掉命令输出,再压成一行;只有几百步的任务,才会把最早的若干步合成一行摘要。飞书消息里从不引导用户去网页。进度每五秒轮询一次。完整历史和 Artifacts 保留在网页对话中。已结束的话题在五分钟后停止轮询,收到新消息时恢复。**消息记录** 列出最近 100 条投递以及未解决的记录;**重试** 保留原有标识。入站消息失败会阻塞该话题后续消息。不确定的出站消息超过 55 分钟后不再重放。停用机器人会暂停投递,但不会取消 Core 中的执行。
收到的消息在 WebSocket ACK 之前写入 SQLite;请求 ID 和冻结的负载在重试与重启后保持不变。工作器原地更新一张简洁进度卡片,将待回答的问题合并到卡片中,结束后改成结果。每次回复只展示当前交流的内容,包括回答提问后继续同一 Core Turn 的情况。正文突出当前回复;等待业务选择时优先展示问题,将补充说明折叠,二维码授权说明保持可见。卡片顶部是带颜色的状态标题栏。处理过程按发生顺序排成一条时间线。只有说明直接显示文字;思考折叠在第一句后面;连续的工具调用折叠成一行(例如“运行 3 条命令”,附总用时、失败次数或“进行中”),展开后每次调用再折叠在它的命令后面,可查看参数、输出、错误和退出码。执行中时,标题栏还会显示当前在做什么和已执行的步数;卡片显示最近四步,更早的步骤折叠进分隔线上方的浅色区块;完成后回复放在最前面,整个过程折叠进浅色的 **处理过程** 区块;失败时出错的那一步紧跟在失败原因下面。长内容按字节限制截取并明确标注。每一步都留在卡片里:为了不超过飞书卡片的大小限制,最早的步骤先去掉命令输出,再压成一行;只有几百步的任务,才会把最早的若干步合成一行摘要。飞书消息里从不引导用户去网页。进度每五秒轮询一次。完整历史和 Artifacts 保留在网页对话中。已结束的话题在五分钟后停止轮询,收到新消息时恢复。**消息记录** 列出最近 100 条投递以及未解决的记录;**重试** 以原有标识重发失败的回复。被拒绝的入站消息会收到原因回复,不会阻塞该话题后续消息。不确定的出站消息超过 55 分钟后不再重放。停用机器人会暂停投递,但不会取消 Core 中的执行。

### 机器人身份 {#bot-identity}

Expand Down
5 changes: 3 additions & 2 deletions example/parsar/server/capability-kinds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ function needers(store, agent) {

// What an Agent's bindings add to its Core Agent and to each of its Sessions.
// A bound bot's identity reaches every conversation; any other variable comes
// from the secret the Agent chose for it.
// from the secret the Agent chose for it. A bot can be bound only to a saved
// Agent, so a missing one is reported in needs_bot, not here.
export function contributions(store, agent) {
const out = {
agent: { tools: [], instructions: [] },
Expand All @@ -137,7 +138,7 @@ export function contributions(store, agent) {
for (const name of entry.credentials || []) {
const label = entry.name || entry.skill_id;
if (Object.hasOwn(botVariables, name)) {
if (!bot) throw new AppError(400, `「${label}」需要飞书机器人身份,请先在飞书接入中给这个 Agent 绑定机器人。`);
out.needs_bot ||= label;
continue;
}
const secret = store.get("secrets", agent.credentials?.[name] || "");
Expand Down
29 changes: 9 additions & 20 deletions example/parsar/server/feishu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,15 @@ export const publicFeishu = ({ app_secret, ...record }) => ({
export function feishuAPI(store, fetchImpl = fetch) {
return async (method, path, body) => {
const retryMatch = path.match(
/^\/app\/feishu\/deliveries\/(inbound|outbound)\/([a-f0-9-]{36})$/,
/^\/app\/feishu\/deliveries\/outbound\/([a-f0-9-]{36})$/,
);
if (retryMatch && method === "PUT" && uuid.test(retryMatch[2])) {
const kind = `feishu_${retryMatch[1]}`,
row = store.get(kind, retryMatch[2]);
if (retryMatch && method === "PUT" && uuid.test(retryMatch[1])) {
const kind = "feishu_outbound",
row = store.get(kind, retryMatch[1]);
if (!row) throw new AppError(404, "投递记录不存在。");
if (row.state !== "failed")
throw new AppError(409, "此记录不需要手动重试。");
if (
kind === "feishu_outbound" &&
sendExpired(row)
)
if (sendExpired(row))
throw new AppError(
409,
"飞书去重窗口已过。请人工检查原话题,不能安全自动重发。",
Expand Down Expand Up @@ -1482,6 +1479,9 @@ export function feishuWorker(store, core, product, makeChannel, configId) {
const retry = (kind, row, error) => {
const attempts = row.attempts + 1;
const definite = !transientError(error);
// A refused message is final, so the chat goes on with the next one.
if (kind === "feishu_inbound" && definite)
return reject(store.get(kind, row.id), error.message || "消息处理失败。");
store.put(kind, {
...store.get(kind, row.id),
attempts,
Expand All @@ -1495,15 +1495,6 @@ export function feishuWorker(store, core, product, makeChannel, configId) {
? 10000
: Math.min(300000, 1000 * 2 ** attempts)),
});
if (kind === "feishu_inbound" && definite) {
const thread = store.get("feishu_threads", row.thread_id);
if (thread)
queueReply(
stableId(`${row.id}:failed`),
thread,
`执行失败\n\n消息处理未完成(${error.message || "服务不可用"}),请重新发送消息重试。`,
);
}
};
const deliver = async (current) => {
for (const row of store.list("feishu_outbound").reverse()) {
Expand Down Expand Up @@ -1623,9 +1614,7 @@ export function feishuWorker(store, core, product, makeChannel, configId) {
["queued", "submitting"].includes(row.state)
)
cancellations.set(row.thread_id, row);
const blocked = new Set(
rows.filter((row) => row.state === "failed").map((row) => row.thread_id),
);
const blocked = new Set();
for (const row of cancellations.values()) {
if (!current()) return;
if (!allowed(row.app_id, row.chat_type)) continue;
Expand Down
Loading
Loading