omp-onlyne 0.0.0-stage → 0.1.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/AGENTS.sample.md +27 -0
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/index.ts +55 -0
- package/package.json +62 -5
- package/src/binding.ts +132 -0
- package/src/bridge-text.ts +109 -0
- package/src/config.ts +300 -0
- package/src/decider-text.ts +420 -0
- package/src/decider.ts +1982 -0
- package/src/drafts.ts +57 -0
- package/src/host-text.ts +86 -0
- package/src/messages.ts +327 -0
- package/src/node.ts +127 -0
- package/src/resources.ts +36 -0
- package/src/runtime.ts +416 -0
- package/src/store.ts +189 -0
- package/src/tern.ts +66 -0
- package/src/todo-native.ts +230 -0
- package/src/todo.ts +896 -0
- package/src/tool-text.ts +133 -0
- package/src/tool.ts +236 -0
- package/src/types.ts +136 -0
- package/src/ui/bootstrap-text.ts +125 -0
- package/src/ui/bootstrap.ts +358 -0
- package/src/ui/command-text.ts +38 -0
- package/src/ui/format.ts +56 -0
- package/src/ui/index.ts +156 -0
- package/src/ui/mail-message.ts +263 -0
- package/src/ui/status.ts +213 -0
- package/src/ui/viewer-text.ts +305 -0
- package/src/ui/viewer.ts +1386 -0
- package/src/urls-text.ts +286 -0
- package/src/urls.ts +1347 -0
package/src/tool-text.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createTranslator } from "@onlyne/node-host/i18n";
|
|
2
|
+
|
|
3
|
+
const en = {
|
|
4
|
+
label: "Onlyne Message",
|
|
5
|
+
"param.delivery_kind": "The delivery intent in natural language",
|
|
6
|
+
"param.msg": "The message body or the local query",
|
|
7
|
+
"param.meta": "Explicit references and operation fields",
|
|
8
|
+
description: `The only surface for Onlyne messages, threads and resources. There is no CLI and nothing else to look up: call this tool with your intent instead of reading docs or source first.
|
|
9
|
+
|
|
10
|
+
Shape: { delivery_kind, msg, meta }. delivery_kind names the intent in plain language ("reply to dpdp's greeting", "show this thread", "read that message"). meta carries references you already know: to, thread, context, in_reply_to {thread,seq} or {sender,id}, relation (reply|followup), mentions, message {sender,id} or {thread,seq}, resource {owner,id}, operation, expected_revision, op, title, scope, seq, attention, cursor, page_size, data, domain_id, owner, since, event_id, operation_id.
|
|
11
|
+
|
|
12
|
+
A send is one call. The result is the delivery fact: admission "accepted" means the receiver recorded the message; wait "timed_out" only means no reply arrived before the wait bound, the message is still delivered; reply_body "withheld" means this context already showed that body. Report the outcome and stop there.
|
|
13
|
+
|
|
14
|
+
A reply arrives by itself. The plugin injects it as an onlyne.inbound message and starts a turn; you do not need to check. Do not poll after a send: no inbox scans, thread reads, evidence queries, log greps or SQLite lookups. Read state only when the user asks about history, or when a specific fact is missing from the result in hand.
|
|
15
|
+
|
|
16
|
+
State reads: read the root \`onlyne://\` for the views this node publishes — neighbors, threads, conversations, inbox, contexts, outbound — instead of guessing a route. A conversation is one exchange with both directions: \`onlyne://conversations\` lists them with the latest message and time, \`onlyne://conversations/<sender>/<id>\` shows every message in it, and one message is at \`onlyne://messages/<sender>/<id>\`. Raw thread entries stay at \`onlyne://threads/<thread-id>/messages/<seq>\`.
|
|
17
|
+
|
|
18
|
+
Resource reads belong to the main OMP session; a subagent session is refused and reads its own message slice instead. meta.scope "resources" selects the resource channel and meta.op names the query:
|
|
19
|
+
|
|
20
|
+
- Inventory: { delivery_kind: "list the resources", msg: "which resources does this node serve", meta: { scope: "resources", op: "list" } }. Without meta.owner the read covers the local node and every owner whose key exchange completed, and each owner answers with its own page or its own error, so one unreachable owner does not hide the others. meta.owner narrows the read to one owner: meta { scope: "resources", op: "list", owner: "<node>" }.
|
|
21
|
+
- One resource: meta { scope: "resources", op: "describe", resource: { owner: "<node>", id: "<resource>" } }.
|
|
22
|
+
- Event log: the same resource with op "log", meta.since as a decimal event id to resume after the last event you read, and meta.page_size (1 to 100) with meta.cursor for the page.
|
|
23
|
+
- One event: the same resource with op "event" and meta.event_id.
|
|
24
|
+
- Operation history: the same resource with op "operations", paged with meta.page_size and meta.cursor.
|
|
25
|
+
- One operation: the same resource with op "status" and meta.operation_id.
|
|
26
|
+
|
|
27
|
+
The same reads are internal paths: \`onlyne://resources\` lists every owner, \`onlyne://resources/<owner>\` one owner, \`onlyne://resources/<owner>/<id>\` one resource, \`onlyne://resources/<owner>/<id>/log\` the event log with \`onlyne://resources/<owner>/<id>/log/<event-id>\` for one event, \`onlyne://resources/<owner>/<id>/operations\` the operation history with \`onlyne://resources/<owner>/<id>/operations/<operation-id>\` for one operation. Page them with ?page_size= and ?cursor=, and resume a log with ?since=.
|
|
28
|
+
|
|
29
|
+
A delivery is stored in the thread it names, and the thread's owner stores it: the initiator owns the thread for the whole conversation, so a member writes into it and the owner admits. A reply names no thread; it lands in the thread that holds the original message. The receipt reports the landing as thread <id> seq <n> and names the message it answers as in_reply_to.
|
|
30
|
+
|
|
31
|
+
meta.to takes one node or a list of nodes. A list is delivered one target at a time and returns one result per target, so a failure on one target does not hide the others; report each result separately.
|
|
32
|
+
|
|
33
|
+
Use cases, each a complete call:
|
|
34
|
+
|
|
35
|
+
- Reply to a message you received. Do not name a thread; the receiver lands the reply where the original lives:
|
|
36
|
+
{ delivery_kind: "answer dpdp's question", msg: "Yes, the second set is fine.", meta: { in_reply_to: { sender: "urn:onlyne:agent:dpdp", id: "<message id>" }, relation: "reply" } }
|
|
37
|
+
- Send a fresh message to one peer: { delivery_kind: "greet lazuli", msg: "Hello from the workbench.", meta: { to: "lazuli" } }
|
|
38
|
+
- Send the same note to two peers: { delivery_kind: "notify both", msg: "Report is up.", meta: { to: ["caerulea", "lazuli"] } } — you get one result per target, and a failure on one does not hide the other.
|
|
39
|
+
- Read the whole exchange with one peer: read \`onlyne://conversations/<sender>/<id>\` from a list you got at \`onlyne://conversations\`. Read one message with \`onlyne://messages/<sender>/<id>\`.
|
|
40
|
+
- Check what happened to a message you sent: read \`onlyne://outbound/<message-id>\`; it shows the landing the receiver reported, whether it is accepted, and the reply if one arrived.
|
|
41
|
+
- Look at what a resource offers before operating it: read \`onlyne://resources\` or \`onlyne://resources/<owner>/<id>\`, then send the operation as { delivery_kind: "list the operations of this resource", msg: "show me what it can do", meta: { resource: { owner: "<node>", id: "<resource>" }, operation: "<name>" } }. meta.data carries the arguments; meta.expected_revision requires the revision you read.
|
|
42
|
+
- In a subagent, answer without sending: { delivery_kind: "draft the reply", msg: "Here is the answer.", meta: { op: "draft", to: "dpdp", relation: "reply", in_reply_to: { sender: "urn:onlyne:agent:dpdp", id: "<message id>" } } } returns { draft_id }. The main session sends exactly that draft: { delivery_kind: "send the drafted reply", msg: "send draft", meta: { draft_id: "<draft id>" } }.
|
|
43
|
+
- When a send fails with no trusted key, do not retry and do not try to pair: the error names both nodes, the responsible holder and the entry in the host surface. Report it and stop.
|
|
44
|
+
|
|
45
|
+
The judge role chooses the action and plan from your intent, so never invent message ids, idempotency keys or revisions. A reply or followup states its relation; omitting it is rejected rather than assumed. A reply targets the original reply authority, so a conflicting to is rejected. A subagent session reads its own slice (onlyne://threads, onlyne://inbox, onlyne://contexts) and returns the result to the main session, which does the sending.`,
|
|
46
|
+
"summary.no_result": "Onlyne returned no structured result.",
|
|
47
|
+
"summary.where": " to {owner}",
|
|
48
|
+
"summary.thread_ref": " (thread {thread} seq {seq}{answering})",
|
|
49
|
+
"summary.answering": ", answering {sender}/{id}",
|
|
50
|
+
"summary.outbound": " · read back: onlyne://outbound/{id}",
|
|
51
|
+
"summary.delivered": "Delivered{where}{facts}{trail} and recorded durably.",
|
|
52
|
+
"summary.delivered_answered": "Delivered{where}{facts}{trail}. {author} answered at seq {seq}.",
|
|
53
|
+
"summary.delivered_withheld": "Delivered{where}{facts}{trail}. {author} answered at seq {seq}, but that body was already shown in this context, so only its arrival is reported.",
|
|
54
|
+
"summary.delivered_timed_out": "Delivered{where}{facts}{trail} and recorded durably. No reply arrived within the wait; it will arrive on its own as an inbound message.",
|
|
55
|
+
"summary.delivered_cancelled": "Delivered{where}{facts}{trail} and recorded durably. The wait was cancelled; a reply will arrive on its own as an inbound message.",
|
|
56
|
+
"summary.not_confirmed": "Not confirmed: {failure}. {next}",
|
|
57
|
+
"summary.retry": "The record stays open and the runtime retries it; sending again would commit a second intent.",
|
|
58
|
+
"summary.recover": "The record stays open for recovery; do not re-send the same intent.",
|
|
59
|
+
"summary.plain": "Onlyne answered; the structured result follows.",
|
|
60
|
+
"failure.invalid_args": "Invalid msg arguments: {message}",
|
|
61
|
+
"failure.not_configured": "Onlyne is not configured for this project. Create .onlyne/config.toml or run /onlyne init.",
|
|
62
|
+
"failure.no_target": "meta.to names no target. Name one peer, or send one call per peer.",
|
|
63
|
+
"draft.ready": "Draft {draft_id} is ready; the main session sends it with meta.draft_id.",
|
|
64
|
+
"multi.delivered": "→ {to}: {summary}",
|
|
65
|
+
"multi.failed": "→ {to}: not sent — {error}",
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
const zh: Record<keyof typeof en, string> = {
|
|
69
|
+
label: "Onlyne 消息",
|
|
70
|
+
"param.delivery_kind": "用自然语言描述的投递意图",
|
|
71
|
+
"param.msg": "消息正文,或本地查询",
|
|
72
|
+
"param.meta": "显式引用与操作字段",
|
|
73
|
+
description: `Onlyne 消息、thread 与资源的唯一入口。没有 CLI,也没有别处要查:把意图交给这个工具,不要先去读文档或源码。
|
|
74
|
+
|
|
75
|
+
形状:{ delivery_kind, msg, meta }。delivery_kind 用日常语言说出意图(“回复 dpdp 的问候”“看看这个 thread”“读那条消息”)。meta 放你已经知道的引用:to、thread、context、in_reply_to {thread,seq} 或 {sender,id}、relation(reply|followup)、mentions、message {sender,id} 或 {thread,seq}、resource {owner,id}、operation、expected_revision、op、title、scope、seq、attention、cursor、page_size、data、domain_id、owner、since、event_id、operation_id。
|
|
76
|
+
|
|
77
|
+
发送是一次调用。结果就是投递事实:admission "accepted" 表示接收方已经记录这条消息;wait "timed_out" 只表示等待期内没有回信,消息本身已经投递;reply_body "withheld" 表示这个上下文已经展示过那段正文。报告结果,到此为止。
|
|
78
|
+
|
|
79
|
+
回信会自己到达。插件把它作为 onlyne.inbound 消息注入并开启一轮对话;你不需要去查。发送之后不要轮询:不要扫信箱、读 thread、查证据、grep 日志或翻 SQLite。只在用户问起历史、或手上的结果缺某个具体事实时才读状态。
|
|
80
|
+
|
|
81
|
+
状态读取:读根路径 \`onlyne://\`,看这个节点发布了哪些视图——neighbors、threads、conversations、inbox、contexts、outbound——不要猜路径。一次 conversation 是一段双向往来:\`onlyne://conversations\` 列出它们以及最新一条消息和时间,\`onlyne://conversations/<sender>/<id>\` 展示其中每一条消息,单条消息在 \`onlyne://messages/<sender>/<id>\`。thread 的原始条目留在 \`onlyne://threads/<thread-id>/messages/<seq>\`。
|
|
82
|
+
|
|
83
|
+
资源读取只属于主 OMP 会话;子代理会话会被拒绝,改为读自己的消息切片。meta.scope 为 "resources" 选中资源通道,meta.op 指明查询:
|
|
84
|
+
|
|
85
|
+
- 清单:{ delivery_kind: "列出资源", msg: "这个节点提供哪些资源", meta: { scope: "resources", op: "list" } }。不带 meta.owner 时,读取覆盖本节点和所有已完成密钥交换的 owner,每个 owner 各自返回一页或各自返回一条错误,所以一个 owner 连不上不会遮住其他 owner。meta.owner 把读取收窄到一个 owner:meta { scope: "resources", op: "list", owner: "<node>" }。
|
|
86
|
+
- 单个资源:meta { scope: "resources", op: "describe", resource: { owner: "<node>", id: "<resource>" } }。
|
|
87
|
+
- 事件日志:同一个 resource 配 op "log",meta.since 是十进制事件 id,用来从你上次读到的事件之后继续,meta.page_size(1 到 100)配 meta.cursor 取页。
|
|
88
|
+
- 单个事件:同一个 resource 配 op "event" 和 meta.event_id。
|
|
89
|
+
- 操作历史:同一个 resource 配 op "operations",用 meta.page_size 和 meta.cursor 分页。
|
|
90
|
+
- 单个操作:同一个 resource 配 op "status" 和 meta.operation_id。
|
|
91
|
+
|
|
92
|
+
同样的读取也是内部路径:\`onlyne://resources\` 列出所有 owner,\`onlyne://resources/<owner>\` 一个 owner,\`onlyne://resources/<owner>/<id>\` 一个资源,\`onlyne://resources/<owner>/<id>/log\` 是事件日志,单个事件用 \`onlyne://resources/<owner>/<id>/log/<event-id>\`,\`onlyne://resources/<owner>/<id>/operations\` 是操作历史,单个操作用 \`onlyne://resources/<owner>/<id>/operations/<operation-id>\`。用 ?page_size= 和 ?cursor= 分页,用 ?since= 续读日志。
|
|
93
|
+
|
|
94
|
+
一次投递存进它指名的 thread,并由 thread 的 owner 保存:发起者在整段对话里都是 owner,所以成员往 thread 里写、由 owner 准入。回信不指名 thread;它落进持有原消息的那个 thread。回执以 thread <id> seq <n> 报告落点,并以 in_reply_to 指出它回复的那条消息。
|
|
95
|
+
|
|
96
|
+
meta.to 接受一个节点或一组节点。一组会逐个目标投递,每个目标各得一条结果,所以某个目标失败不会掩盖其他目标;逐个报告结果。
|
|
97
|
+
|
|
98
|
+
用例,每个都是完整调用:
|
|
99
|
+
|
|
100
|
+
- 回复你收到的消息。不要指名 thread;接收方会把回信放到原消息所在的 thread:
|
|
101
|
+
{ delivery_kind: "回答 dpdp 的问题", msg: "可以,第二组没问题。", meta: { in_reply_to: { sender: "urn:onlyne:agent:dpdp", id: "<message id>" }, relation: "reply" } }
|
|
102
|
+
- 给一个 peer 发新消息:{ delivery_kind: "向 lazuli 问好", msg: "来自工作台的问候。", meta: { to: "lazuli" } }
|
|
103
|
+
- 把同一段话发给两个 peer:{ delivery_kind: "通知双方", msg: "报告已上传。", meta: { to: ["caerulea", "lazuli"] } } —— 每个目标各得一条结果,某个目标失败不会掩盖另一个。
|
|
104
|
+
- 读与某个 peer 的整段往来:先读 \`onlyne://conversations\` 拿到一条,再读 \`onlyne://conversations/<sender>/<id>\`。读单条消息用 \`onlyne://messages/<sender>/<id>\`。
|
|
105
|
+
- 查你发出的消息后来怎样了:读 \`onlyne://outbound/<message-id>\`;它显示接收方报告的落点、是否被接纳,以及回信(如果到了)。
|
|
106
|
+
- 操作资源前先看它提供什么:读 \`onlyne://resources\` 或 \`onlyne://resources/<owner>/<id>\`,然后发操作 { delivery_kind: "列出该资源的操作", msg: "看看它能做什么", meta: { resource: { owner: "<node>", id: "<resource>" }, operation: "<name>" } }。meta.data 带参数;meta.expected_revision 要求版本与你读到的一致。
|
|
107
|
+
- 在子代理里,只写不发:{ delivery_kind: "起草回复", msg: "这是答案。", meta: { op: "draft", to: "dpdp", relation: "reply", in_reply_to: { sender: "urn:onlyne:agent:dpdp", id: "<message id>" } } } 返回 { draft_id }。主会话原样发出这份草稿:{ delivery_kind: "发出起草的回复", msg: "发送草稿", meta: { draft_id: "<draft id>" } }。
|
|
108
|
+
- 发送失败且没有可信密钥时,不要重试也不要尝试配对:错误里写明了两个节点、负责的持有者和宿主界面上的入口。报告它,然后停下。
|
|
109
|
+
|
|
110
|
+
动作和执行计划由 judge 角色根据你的意图选择,所以永远不要自己编 message id、幂等键或版本号。回信或 followup 必须说明关系;省略会被拒绝,而不是被假定。回信只针对原来的回信权限方,所以冲突的 to 会被拒绝。子代理会话读自己的切片(onlyne://threads、onlyne://inbox、onlyne://contexts),把结果交回主会话,由主会话发送。`,
|
|
111
|
+
"summary.no_result": "Onlyne 没有返回结构化结果。",
|
|
112
|
+
"summary.where": ",投递给 {owner}",
|
|
113
|
+
"summary.thread_ref": "(thread {thread} seq {seq}{answering})",
|
|
114
|
+
"summary.answering": ",回复 {sender}/{id}",
|
|
115
|
+
"summary.outbound": ";读回: onlyne://outbound/{id}",
|
|
116
|
+
"summary.delivered": "已送达{where}{facts},并已持久记录{trail}。",
|
|
117
|
+
"summary.delivered_answered": "已送达{where}{facts},并已持久记录{trail}。{author} 在 seq {seq} 回复了。",
|
|
118
|
+
"summary.delivered_withheld": "已送达{where}{facts},并已持久记录{trail}。{author} 在 seq {seq} 回复了,但那段正文已经在这个上下文里展示过,所以只报告它到达。",
|
|
119
|
+
"summary.delivered_timed_out": "已送达{where}{facts},并已持久记录{trail}。等待期内没有回信;回信会作为入站消息自己到达。",
|
|
120
|
+
"summary.delivered_cancelled": "已送达{where}{facts},并已持久记录{trail}。等待被取消;回信会作为入站消息自己到达。",
|
|
121
|
+
"summary.not_confirmed": "未确认:{failure}。{next}",
|
|
122
|
+
"summary.retry": "记录保持打开,运行时自己重试;再发一次会提交第二个意图。",
|
|
123
|
+
"summary.recover": "记录保持打开,等待恢复;不要再发同一个意图。",
|
|
124
|
+
"summary.plain": "Onlyne 已应答;结构化结果如下。",
|
|
125
|
+
"failure.invalid_args": "msg 参数无效:{message}",
|
|
126
|
+
"failure.not_configured": "这个项目没有配置 Onlyne。创建 .onlyne/config.toml,或运行 /onlyne init。",
|
|
127
|
+
"failure.no_target": "meta.to 没有指明目标。写一个 peer,或每个 peer 各发一次。",
|
|
128
|
+
"draft.ready": "草稿 {draft_id} 已就绪;主会话用 meta.draft_id 发出它。",
|
|
129
|
+
"multi.delivered": "→ {to}: {summary}",
|
|
130
|
+
"multi.failed": "→ {to}: 未发送 —— {error}",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const toolText = createTranslator({ en, "zh-CN": zh });
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type ExtensionAPI, type ExtensionContext, type SessionEntry, z } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import type { Json, MsgInput, SendOutcome } from "@onlyne/lib";
|
|
4
|
+
import { runDecision } from "./decider.ts";
|
|
5
|
+
import { createDraft, sendDraft } from "./drafts.ts";
|
|
6
|
+
import { observeToolResult } from "./messages.ts";
|
|
7
|
+
import { getRuntime } from "./runtime.ts";
|
|
8
|
+
import { toolText } from "./tool-text.ts";
|
|
9
|
+
|
|
10
|
+
/** Custom entry naming a reply an `await` returned, left pending for the host bridge to confirm. */
|
|
11
|
+
export const AWAIT_RESULT = "onlyne.await-result";
|
|
12
|
+
/** The custom entry type naming an await result the model has actually read. */
|
|
13
|
+
export const AWAIT_CONSUMED = "onlyne.await-consumed";
|
|
14
|
+
|
|
15
|
+
export function registerMsgTool(pi: ExtensionAPI): void {
|
|
16
|
+
const parameters = z.object({
|
|
17
|
+
delivery_kind: z.string().min(1).describe(toolText("param.delivery_kind")),
|
|
18
|
+
msg: z.string().describe(toolText("param.msg")),
|
|
19
|
+
meta: z.record(z.string(), z.unknown()).optional().describe(toolText("param.meta")),
|
|
20
|
+
});
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: "msg",
|
|
23
|
+
label: toolText("label"),
|
|
24
|
+
description: toolText("description"),
|
|
25
|
+
parameters,
|
|
26
|
+
approval: "read",
|
|
27
|
+
strict: true,
|
|
28
|
+
execute: async (
|
|
29
|
+
toolCallId,
|
|
30
|
+
raw,
|
|
31
|
+
signal,
|
|
32
|
+
_onUpdate,
|
|
33
|
+
ctx,
|
|
34
|
+
): Promise<AgentToolResult<{ result: unknown }>> => {
|
|
35
|
+
const parsed = parameters.safeParse(raw);
|
|
36
|
+
if (!parsed.success) return failure(toolText("failure.invalid_args", { message: parsed.error.message }));
|
|
37
|
+
const runtime = getRuntime();
|
|
38
|
+
if (runtime === undefined) return failure(toolText("failure.not_configured"));
|
|
39
|
+
const input: MsgInput = {
|
|
40
|
+
delivery_kind: parsed.data.delivery_kind,
|
|
41
|
+
msg: parsed.data.msg,
|
|
42
|
+
...(parsed.data.meta === undefined ? {} : { meta: parsed.data.meta as Record<string, Json> }),
|
|
43
|
+
};
|
|
44
|
+
const scope = {
|
|
45
|
+
ctx,
|
|
46
|
+
input,
|
|
47
|
+
...(signal === undefined ? {} : { signal }),
|
|
48
|
+
...(ctx.agent.kind === "sub" ? { readOnly: true } : {}),
|
|
49
|
+
};
|
|
50
|
+
const started = Date.now();
|
|
51
|
+
try {
|
|
52
|
+
const op = parsed.data.meta?.op;
|
|
53
|
+
if (op === "draft") {
|
|
54
|
+
const result = await createDraft(runtime, ctx, input, signal);
|
|
55
|
+
pi.logger.info("onlyne.draft.created", { ms: Date.now() - started });
|
|
56
|
+
// createDraft is typed unknown but always returns the draft record it just stored.
|
|
57
|
+
const draft = result as { draft_id: string };
|
|
58
|
+
return { content: [{ type: "text", text: toolText("draft.ready", { draft_id: draft.draft_id }) }], details: { result } };
|
|
59
|
+
}
|
|
60
|
+
if (typeof parsed.data.meta?.draft_id === "string") {
|
|
61
|
+
const result = await sendDraft(runtime, ctx, parsed.data.meta.draft_id, signal);
|
|
62
|
+
recordAwaitResult(pi, toolCallId, result, ctx);
|
|
63
|
+
return { content: [{ type: "text", text: `${summary(result)}\n\n${JSON.stringify(visibleResult(result), null, 2)}` }], details: { result: visibleResult(result) } };
|
|
64
|
+
}
|
|
65
|
+
const targets = parsed.data.meta?.to;
|
|
66
|
+
if (Array.isArray(targets)) {
|
|
67
|
+
if (targets.length === 0) return failure(toolText("failure.no_target"));
|
|
68
|
+
const rows: Array<{ to: unknown; outcome?: unknown; error?: string }> = [];
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
const one: MsgInput = { ...input, meta: { ...input.meta, to: target as Json } };
|
|
71
|
+
const oneScope = { ...scope, input: one };
|
|
72
|
+
try {
|
|
73
|
+
rows.push({ to: target, outcome: await runDecision(oneScope, () => runtime.node.client.msg(one, {
|
|
74
|
+
...(signal === undefined ? {} : { signal }), context: ctx.agent.id,
|
|
75
|
+
})) });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
rows.push({ to: target, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const text = rows.map(row => row.error === undefined
|
|
81
|
+
? toolText("multi.delivered", { to: String(row.to), summary: summary(row.outcome) })
|
|
82
|
+
: toolText("multi.failed", { to: String(row.to), error: row.error })).join("\n");
|
|
83
|
+
pi.logger.info("onlyne.msg.multi", { targets: rows.length, failed: rows.filter(row => row.error !== undefined).length });
|
|
84
|
+
return { content: [{ type: "text", text: `${text}\n\n${JSON.stringify(rows, null, 2)}` }], details: { result: rows } };
|
|
85
|
+
}
|
|
86
|
+
const result = await runDecision(scope, () =>
|
|
87
|
+
runtime.node.client.msg(input, {
|
|
88
|
+
...(signal === undefined ? {} : { signal }),
|
|
89
|
+
context: ctx.agent.id,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
recordAwaitResult(pi, toolCallId, result, ctx);
|
|
93
|
+
const visible = visibleResult(result);
|
|
94
|
+
pi.logger.info("onlyne.msg.result", { kind: input.delivery_kind, ms: Date.now() - started, ...outcomeFacts(result) });
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: `${summary(result)}\n\n${JSON.stringify(visible, null, 2)}` }],
|
|
97
|
+
details: { result: visible },
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
pi.logger.warn("onlyne.msg.failed", {
|
|
101
|
+
kind: input.delivery_kind, ms: Date.now() - started,
|
|
102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The outcome as one sentence. A structured result invites a key-by-key search —
|
|
112
|
+
* `wait: timed_out` beside `admission: accepted` reads as a contradiction until the
|
|
113
|
+
* reader knows the contract — so the delivery fact is stated first and the JSON below
|
|
114
|
+
* it is for the rare case that needs a machine-readable detail.
|
|
115
|
+
*/
|
|
116
|
+
export function summary(result: unknown): string {
|
|
117
|
+
if (result === null || typeof result !== "object") return toolText("summary.no_result");
|
|
118
|
+
const outcome = result as Partial<SendOutcome>;
|
|
119
|
+
const owner = outcome.receipt?.thread_ref?.owner ?? outcome.reply?.thread_ref.owner;
|
|
120
|
+
const where = owner === undefined ? "" : toolText("summary.where", { owner });
|
|
121
|
+
// The answer is part of the delivery fact: a reply the receipt cannot attribute is one the
|
|
122
|
+
// sender cannot prove landed on the message it meant (cyanea report 2, problem 2).
|
|
123
|
+
const answers = outcome.receipt?.in_reply_to;
|
|
124
|
+
const answering = answers === undefined ? "" : toolText("summary.answering", { sender: answers.sender, id: answers.id });
|
|
125
|
+
const facts = outcome.receipt === undefined
|
|
126
|
+
? ""
|
|
127
|
+
: toolText("summary.thread_ref", { thread: outcome.receipt.thread_ref.id, seq: outcome.receipt.seq, answering });
|
|
128
|
+
// The receipt names a thread the receiver owns, which is not a local object; the outbound
|
|
129
|
+
// record is. Point the reader at the handle that opens (dpdp report, problem 1).
|
|
130
|
+
const trail = outcome.receipt === undefined ? "" : toolText("summary.outbound", { id: outcome.receipt.message_ref.id });
|
|
131
|
+
if (outcome.admission !== undefined) {
|
|
132
|
+
if (outcome.admission !== "accepted") {
|
|
133
|
+
const error = outcome.error;
|
|
134
|
+
const failure = error === undefined ? outcome.admission : `${error.code}: ${error.message}`;
|
|
135
|
+
// `retryable` belongs to the runtime, not to the caller: the record stays open and the
|
|
136
|
+
// runtime drives it again, while a second send commits a second intent.
|
|
137
|
+
const next = error?.retryable === true
|
|
138
|
+
? toolText("summary.retry")
|
|
139
|
+
: toolText("summary.recover");
|
|
140
|
+
return toolText("summary.not_confirmed", { failure, next });
|
|
141
|
+
}
|
|
142
|
+
if (outcome.wait === "answered" && outcome.reply !== undefined) {
|
|
143
|
+
const answer = { author: outcome.reply.authenticated_author, seq: outcome.reply.seq, where, facts, trail };
|
|
144
|
+
return outcome.reply_body === "withheld"
|
|
145
|
+
? toolText("summary.delivered_withheld", answer)
|
|
146
|
+
: toolText("summary.delivered_answered", answer);
|
|
147
|
+
}
|
|
148
|
+
if (outcome.wait === "timed_out") {
|
|
149
|
+
return toolText("summary.delivered_timed_out", { where, facts, trail });
|
|
150
|
+
}
|
|
151
|
+
if (outcome.wait === "cancelled") {
|
|
152
|
+
return toolText("summary.delivered_cancelled", { where, facts, trail });
|
|
153
|
+
}
|
|
154
|
+
return toolText("summary.delivered", { where, facts, trail });
|
|
155
|
+
}
|
|
156
|
+
return toolText("summary.plain");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The facts one `msg` call produced, without the reply body. */
|
|
160
|
+
function outcomeFacts(result: unknown): Record<string, unknown> {
|
|
161
|
+
if (result === null || typeof result !== "object") return {};
|
|
162
|
+
const outcome = result as Partial<SendOutcome>;
|
|
163
|
+
return {
|
|
164
|
+
...(outcome.admission === undefined ? {} : { admission: outcome.admission }),
|
|
165
|
+
...(outcome.wait === undefined ? {} : { wait: outcome.wait }),
|
|
166
|
+
...(outcome.reply_body === undefined ? {} : { withheld: outcome.reply_body === "withheld" }),
|
|
167
|
+
...(outcome.receipt?.seq === undefined ? {} : { seq: outcome.receipt.seq }),
|
|
168
|
+
...(outcome.receipt?.in_reply_to === undefined ? {} : { reply_to: `${outcome.receipt.in_reply_to.sender}/${outcome.receipt.in_reply_to.id}` }),
|
|
169
|
+
...(outcome.reply === undefined ? {} : { entry: `${outcome.reply.thread_ref.owner}/${outcome.reply.thread_ref.id}/${outcome.reply.seq}` }),
|
|
170
|
+
...(outcome.error === undefined ? {} : { code: outcome.error.code }),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An `await` that returned a reply is recorded as a pending custom entry. The
|
|
176
|
+
* host bridge confirms consumption when the model actually reads the result;
|
|
177
|
+
* submitting the tool result proves nothing (RFC 0008 §5).
|
|
178
|
+
*/
|
|
179
|
+
function recordAwaitResult(pi: ExtensionAPI, toolCallId: string, result: unknown, ctx: ExtensionContext): void {
|
|
180
|
+
if (result === null || typeof result !== "object") return;
|
|
181
|
+
if ((result as Partial<SendOutcome>).reply_body === "withheld") return;
|
|
182
|
+
const reply = (result as Partial<SendOutcome>).reply;
|
|
183
|
+
if (reply === null || reply === undefined || typeof reply !== "object") return;
|
|
184
|
+
if (typeof reply.seq !== "string" || typeof reply.thread_ref?.owner !== "string" || typeof reply.thread_ref.id !== "string") return;
|
|
185
|
+
if (typeof reply.message?.sender !== "string" || typeof reply.message.id !== "string") return;
|
|
186
|
+
observeToolResult(toolCallId, reply, ctx.agent.id);
|
|
187
|
+
pi.appendEntry(AWAIT_RESULT, {
|
|
188
|
+
tool_call_id: toolCallId,
|
|
189
|
+
context: ctx.agent.id,
|
|
190
|
+
ref: { sender: reply.message.sender, id: reply.message.id },
|
|
191
|
+
thread_ref: reply.thread_ref,
|
|
192
|
+
seq: reply.seq,
|
|
193
|
+
state: "pending",
|
|
194
|
+
at: new Date().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function visibleResult(result: unknown): unknown {
|
|
199
|
+
if (result === null || typeof result !== "object") return result;
|
|
200
|
+
const outcome = result as Partial<SendOutcome>;
|
|
201
|
+
if (outcome.reply_body !== "withheld" || outcome.reply === undefined) return result;
|
|
202
|
+
const { thread_ref, seq, authenticated_author, reply_authority, at, message } = outcome.reply;
|
|
203
|
+
return {
|
|
204
|
+
...outcome,
|
|
205
|
+
reply: {
|
|
206
|
+
thread_ref, seq, authenticated_author, reply_authority, at,
|
|
207
|
+
message: { sender: message.sender, id: message.id },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function pendingAwaitResults(branch: readonly SessionEntry[]): Array<{ toolCallId: string; context: string; entry: NonNullable<SendOutcome["reply"]> }> {
|
|
213
|
+
const consumed = new Set(branch.flatMap(entry => entry.type === "custom" && entry.customType === AWAIT_CONSUMED
|
|
214
|
+
&& typeof (entry.data as { tool_call_id?: unknown })?.tool_call_id === "string"
|
|
215
|
+
? [(entry.data as { tool_call_id: string }).tool_call_id] : []));
|
|
216
|
+
const results = new Map(branch.flatMap(entry => entry.type === "message" && entry.message.role === "toolResult"
|
|
217
|
+
? [[entry.message.toolCallId, entry.message] as const] : []));
|
|
218
|
+
return branch.flatMap(entry => {
|
|
219
|
+
if (entry.type !== "custom" || entry.customType !== AWAIT_RESULT) return [];
|
|
220
|
+
const data = entry.data as { tool_call_id?: unknown; context?: unknown; ref?: { sender?: unknown; id?: unknown } } | undefined;
|
|
221
|
+
if (typeof data?.tool_call_id !== "string" || typeof data.context !== "string" || consumed.has(data.tool_call_id)) return [];
|
|
222
|
+
const outcome = (results.get(data.tool_call_id)?.details as { result?: Partial<SendOutcome> } | undefined)?.result;
|
|
223
|
+
const reply = outcome?.reply;
|
|
224
|
+
if (outcome?.reply_body === "withheld" || typeof reply?.message?.msg !== "string"
|
|
225
|
+
|| reply.message.sender !== data.ref?.sender || reply.message.id !== data.ref?.id) return [];
|
|
226
|
+
return [{ toolCallId: data.tool_call_id, context: data.context, entry: reply }];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function failure(text: string): AgentToolResult<{ result: unknown }> {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text }],
|
|
233
|
+
details: { result: undefined },
|
|
234
|
+
isError: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { MessageRef, NodeId, ThreadRef, TrustPhase, TrustSeed } from "@onlyne/lib";
|
|
2
|
+
import type { Locale } from "@onlyne/node-host/i18n";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Project-scoped configuration in `<project>/.onlyne/config.toml`.
|
|
6
|
+
*
|
|
7
|
+
* Delivery decisions are made by OMP's `judge` model role, so no model
|
|
8
|
+
* endpoint or model name belongs here.
|
|
9
|
+
*/
|
|
10
|
+
export interface OnlyneConfig {
|
|
11
|
+
path: string;
|
|
12
|
+
language?: Locale;
|
|
13
|
+
/** Protocol membership, independent from the node identity. */
|
|
14
|
+
region: string;
|
|
15
|
+
node: {
|
|
16
|
+
id: NodeId;
|
|
17
|
+
key_ref?: string;
|
|
18
|
+
/** Where the node identity lives: the project's `.onlyne/keys`, the user's key store, or no key at all. */
|
|
19
|
+
key_scope?: "project" | "global" | "none";
|
|
20
|
+
};
|
|
21
|
+
server: {
|
|
22
|
+
bind: string;
|
|
23
|
+
/** The externally reachable base URL when a reverse proxy fronts the node. */
|
|
24
|
+
public_url?: string;
|
|
25
|
+
};
|
|
26
|
+
threads: {
|
|
27
|
+
page_size: number;
|
|
28
|
+
};
|
|
29
|
+
/** Bootstrap contact seeds the node reaches at every start. */
|
|
30
|
+
discovery?: {
|
|
31
|
+
seeds?: string[];
|
|
32
|
+
};
|
|
33
|
+
/** Pinned peer fingerprints; an arriving key that matches is trusted without a prompt. */
|
|
34
|
+
trust?: TrustSeed[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One execution context the plugin registered for a delivered message. */
|
|
38
|
+
export interface ContextRecord {
|
|
39
|
+
alias: string;
|
|
40
|
+
node_id: NodeId;
|
|
41
|
+
main_agent_id: string;
|
|
42
|
+
source?: { sender: NodeId; id: string };
|
|
43
|
+
agent_id?: string;
|
|
44
|
+
session_file?: string;
|
|
45
|
+
parent_id?: string;
|
|
46
|
+
thread_ref?: ThreadRef;
|
|
47
|
+
summary: string;
|
|
48
|
+
state: "unbound" | "idle" | "running" | "parked" | "lost";
|
|
49
|
+
created_at: string;
|
|
50
|
+
bound_at?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One row of the viewer's queue page. */
|
|
54
|
+
export interface QueueItem {
|
|
55
|
+
id: string;
|
|
56
|
+
kind: "inbound" | "outbound" | "context" | "decision";
|
|
57
|
+
label: string;
|
|
58
|
+
detail: string;
|
|
59
|
+
at?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One message row in the viewer's chat page. */
|
|
63
|
+
export interface ViewerMessage {
|
|
64
|
+
ref: MessageRef;
|
|
65
|
+
seq: string;
|
|
66
|
+
direction: "in" | "out";
|
|
67
|
+
author: string;
|
|
68
|
+
at: string;
|
|
69
|
+
text: string;
|
|
70
|
+
relation?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One thread row in the viewer's chat page. */
|
|
74
|
+
export interface ViewerThread {
|
|
75
|
+
ref: ThreadRef;
|
|
76
|
+
alias: string;
|
|
77
|
+
title: string;
|
|
78
|
+
state: "open" | "closed";
|
|
79
|
+
unread: number;
|
|
80
|
+
last_seq: string;
|
|
81
|
+
messages: ViewerMessage[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One discovery record row the viewer lists as an automatic-discovery candidate. */
|
|
85
|
+
export interface ViewerDiscovery {
|
|
86
|
+
node_id: NodeId;
|
|
87
|
+
instance_id: string;
|
|
88
|
+
base_url: string;
|
|
89
|
+
source: string;
|
|
90
|
+
region?: string;
|
|
91
|
+
membership_state?: "active" | "left";
|
|
92
|
+
verified_at?: string;
|
|
93
|
+
expires_at: string;
|
|
94
|
+
last_seen: string;
|
|
95
|
+
error?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One reachable peer the viewer lists on its contacts page. */
|
|
99
|
+
export interface ViewerContact {
|
|
100
|
+
node_id: NodeId;
|
|
101
|
+
instance_id: string;
|
|
102
|
+
base_url: string;
|
|
103
|
+
region?: string;
|
|
104
|
+
last_seen: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read-only snapshot the viewer renders. */
|
|
108
|
+
export interface ViewerSnapshot {
|
|
109
|
+
region?: string;
|
|
110
|
+
endpoint?: string;
|
|
111
|
+
queue: QueueItem[];
|
|
112
|
+
threads: ViewerThread[];
|
|
113
|
+
contexts: ContextRecord[];
|
|
114
|
+
discovery: ViewerDiscovery[];
|
|
115
|
+
contacts: ViewerContact[];
|
|
116
|
+
discovery_error?: string;
|
|
117
|
+
/** Every trust row, projected through `trustPhase`: the only stage this package renders. */
|
|
118
|
+
trust?: Array<{
|
|
119
|
+
node_id: NodeId;
|
|
120
|
+
fingerprint: string;
|
|
121
|
+
phase: TrustPhase;
|
|
122
|
+
base_url: string;
|
|
123
|
+
}>;
|
|
124
|
+
identity?: { public_key: string; fingerprint: string };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The one-line runtime status a status widget renders. */
|
|
128
|
+
export interface OnlyneStatus {
|
|
129
|
+
region: string;
|
|
130
|
+
node_id: NodeId;
|
|
131
|
+
nodes: number;
|
|
132
|
+
neighbors: number;
|
|
133
|
+
alerts: number;
|
|
134
|
+
unread: number;
|
|
135
|
+
discovery_error?: string;
|
|
136
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createTranslator } from "@onlyne/node-host/i18n";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Text of the first-run form, the status row, the mail card and the relative
|
|
5
|
+
* time phrases: one UI layer, one catalog. Keys are English source text; the
|
|
6
|
+
* translator reads the process locale on every call, so nothing here is frozen
|
|
7
|
+
* at import time.
|
|
8
|
+
*/
|
|
9
|
+
const en = {
|
|
10
|
+
"bootstrap.title": "Onlyne init",
|
|
11
|
+
"bootstrap.overlayHead": "Onlyne init",
|
|
12
|
+
"bootstrap.field.region": "Region",
|
|
13
|
+
"bootstrap.field.nodeId": "Node id",
|
|
14
|
+
"bootstrap.field.bind": "Server bind",
|
|
15
|
+
"bootstrap.field.keyScope": "Node key (project/global/none)",
|
|
16
|
+
"bootstrap.field.pageSize": "Page size",
|
|
17
|
+
"bootstrap.field.contact": "Node addresses (optional)",
|
|
18
|
+
"bootstrap.hint.region": "A short name is stored as urn:onlyne:region:<name>",
|
|
19
|
+
"bootstrap.hint.nodeId": "A short name is stored as urn:onlyne:agent:<name>",
|
|
20
|
+
"bootstrap.hint.bind": "127.0.0.1",
|
|
21
|
+
"bootstrap.hint.keyScope":
|
|
22
|
+
"project: keys in .onlyne/keys (must be git-ignored); global: keys in ~/.config/onlyne/keys; none: loopback development only",
|
|
23
|
+
"bootstrap.hint.pageSize": "1..100, default 50",
|
|
24
|
+
"bootstrap.hint.contact":
|
|
25
|
+
"Nodes discovery cannot reach are contacted by address; stored as discovery.seeds and contacted on every start. Separate addresses with commas",
|
|
26
|
+
"bootstrap.empty": "[empty]",
|
|
27
|
+
"bootstrap.regionMissing": "Region is required; a short name is stored as urn:onlyne:region:<name>",
|
|
28
|
+
"bootstrap.regionPreview": "Will be stored as {uri}",
|
|
29
|
+
"bootstrap.regionEmpty": "Region must not be empty.",
|
|
30
|
+
"bootstrap.keyScopeInvalid": "Node key must be project, global or none.",
|
|
31
|
+
"bootstrap.seedFailed": "Configuration saved and the node started, but {seed} did not connect: {reason}",
|
|
32
|
+
"bootstrap.started": "Onlyne initialized: {region}",
|
|
33
|
+
"bootstrap.startFailed": "Configuration written, start failed: {reason}",
|
|
34
|
+
"bootstrap.notSaved": "Not saved: {reason}",
|
|
35
|
+
"bootstrap.save": "Save and start",
|
|
36
|
+
"bootstrap.saving": "Saving…",
|
|
37
|
+
"bootstrap.cancel": "Cancel",
|
|
38
|
+
"bootstrap.keys": "Tab/Shift+Tab newline · Enter next field · Ctrl+Enter save · Esc cancel",
|
|
39
|
+
"bootstrap.header": "{source}. Saving starts this node and publishes discovery.{existing}",
|
|
40
|
+
"bootstrap.headerExisting": " A project configuration already exists and saving overwrites it.",
|
|
41
|
+
"status.nodes": "nodes",
|
|
42
|
+
"status.neighbors": "neighbors",
|
|
43
|
+
"status.alert": "alert",
|
|
44
|
+
"status.unread": "unread",
|
|
45
|
+
"status.discovery": "discovery",
|
|
46
|
+
"status.unconfigured": "unconfigured",
|
|
47
|
+
"status.starting": "starting",
|
|
48
|
+
"mail.supplement": "supplement",
|
|
49
|
+
"mail.routing": "forwarded",
|
|
50
|
+
"mail.letter": "letter",
|
|
51
|
+
"mail.from": "From",
|
|
52
|
+
"mail.date": "Date",
|
|
53
|
+
"mail.thread": "Thread",
|
|
54
|
+
"mail.relation": "Relation",
|
|
55
|
+
"mail.inReplyTo": "In-Reply-To",
|
|
56
|
+
"mail.delivery": "Delivery",
|
|
57
|
+
"mail.status": "Status",
|
|
58
|
+
"mail.consumed": " · consumed {state}",
|
|
59
|
+
"mail.rawRecord": "Raw record",
|
|
60
|
+
"mail.wholeThread": "Whole thread",
|
|
61
|
+
"time.justNow": "just now",
|
|
62
|
+
"time.minutesAgo": "{count} min ago",
|
|
63
|
+
"time.hoursAgo": "{count} h ago",
|
|
64
|
+
"time.daysAgo": "{count} d ago",
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
const zh: Record<keyof typeof en, string> = {
|
|
68
|
+
"bootstrap.title": "Onlyne init",
|
|
69
|
+
"bootstrap.overlayHead": "Onlyne init",
|
|
70
|
+
"bootstrap.field.region": "Region",
|
|
71
|
+
"bootstrap.field.nodeId": "Node id",
|
|
72
|
+
"bootstrap.field.bind": "Server bind",
|
|
73
|
+
"bootstrap.field.keyScope": "Node key (project/global/none)",
|
|
74
|
+
"bootstrap.field.pageSize": "Page size",
|
|
75
|
+
"bootstrap.field.contact": "节点地址(可选)",
|
|
76
|
+
"bootstrap.hint.region": "短名保存为 urn:onlyne:region:<短名>",
|
|
77
|
+
"bootstrap.hint.nodeId": "短名保存为 urn:onlyne:agent:<短名>",
|
|
78
|
+
"bootstrap.hint.bind": "127.0.0.1",
|
|
79
|
+
"bootstrap.hint.keyScope":
|
|
80
|
+
"project:密钥存 .onlyne/keys(git 内需 ignore);global:存 ~/.config/onlyne/keys;none:回环开发绑定",
|
|
81
|
+
"bootstrap.hint.pageSize": "1..100,默认 50",
|
|
82
|
+
"bootstrap.hint.contact": "自动发现到不了的节点按地址联系;保存为 discovery.seeds,每次启动都会联系。多个地址用逗号分隔",
|
|
83
|
+
"bootstrap.empty": "[空]",
|
|
84
|
+
"bootstrap.regionMissing": "Region 必填;短名将保存为 urn:onlyne:region:<短名>",
|
|
85
|
+
"bootstrap.regionPreview": "将保存为 {uri}",
|
|
86
|
+
"bootstrap.regionEmpty": "Region 不能为空。",
|
|
87
|
+
"bootstrap.keyScopeInvalid": "Node key 只能是 project、global 或 none。",
|
|
88
|
+
"bootstrap.seedFailed": "配置已保存并启动,但 {seed} 没连上:{reason}",
|
|
89
|
+
"bootstrap.started": "Onlyne 已初始化:{region}",
|
|
90
|
+
"bootstrap.startFailed": "配置已写入,启动失败:{reason}",
|
|
91
|
+
"bootstrap.notSaved": "未保存:{reason}",
|
|
92
|
+
"bootstrap.save": "保存并启动",
|
|
93
|
+
"bootstrap.saving": "保存中…",
|
|
94
|
+
"bootstrap.cancel": "取消",
|
|
95
|
+
"bootstrap.keys": "Tab/Shift+Tab 换行 · Enter 下一项 · Ctrl+Enter 保存 · Esc 取消",
|
|
96
|
+
"bootstrap.header": "{source}。保存后启动本节点并发布发现。{existing}",
|
|
97
|
+
"bootstrap.headerExisting": "已存在项目配置,保存将覆盖。",
|
|
98
|
+
"status.nodes": "节点",
|
|
99
|
+
"status.neighbors": "邻居",
|
|
100
|
+
"status.alert": "提醒",
|
|
101
|
+
"status.unread": "未读",
|
|
102
|
+
"status.discovery": "发现",
|
|
103
|
+
"status.unconfigured": "未配置",
|
|
104
|
+
"status.starting": "启动中",
|
|
105
|
+
"mail.supplement": "补笺",
|
|
106
|
+
"mail.routing": "转递",
|
|
107
|
+
"mail.letter": "来信",
|
|
108
|
+
"mail.from": "发件人",
|
|
109
|
+
"mail.date": "日期",
|
|
110
|
+
"mail.thread": "线程",
|
|
111
|
+
"mail.relation": "关系",
|
|
112
|
+
"mail.inReplyTo": "回复",
|
|
113
|
+
"mail.delivery": "投送",
|
|
114
|
+
"mail.status": "状态",
|
|
115
|
+
"mail.consumed": " · 已消费 {state}",
|
|
116
|
+
"mail.rawRecord": "原始记录",
|
|
117
|
+
"mail.wholeThread": "整段对话",
|
|
118
|
+
"time.justNow": "刚刚",
|
|
119
|
+
"time.minutesAgo": "{count} 分钟前",
|
|
120
|
+
"time.hoursAgo": "{count} 小时前",
|
|
121
|
+
"time.daysAgo": "{count} 天前",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const t = createTranslator({ en, "zh-CN": zh });
|
|
125
|
+
export type UiTextKey = keyof typeof en;
|