omp-onlyne 0.2.0 → 0.3.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/README.md +3 -3
- package/package.json +3 -3
- package/src/bridge-text.ts +4 -8
- package/src/config.ts +6 -4
- package/src/decider-text.ts +3 -6
- package/src/decider.ts +124 -670
- package/src/drafts.ts +68 -19
- package/src/guard.ts +107 -0
- package/src/handshake.ts +2 -2
- package/src/messages.ts +24 -6
- package/src/plan.ts +133 -0
- package/src/questions.ts +250 -0
- package/src/resolve.ts +387 -0
- package/src/resources.ts +1 -1
- package/src/runtime.ts +201 -25
- package/src/tool.ts +11 -2
- package/src/types.ts +60 -66
- package/src/ui/bootstrap-text.ts +24 -0
- package/src/ui/bootstrap.ts +77 -3
- package/src/ui/index.ts +6 -2
- package/src/ui/mail-message.ts +5 -1
- package/src/ui/viewer-text.ts +112 -4
- package/src/ui/viewer.ts +629 -57
- package/src/urls-text.ts +6 -0
- package/src/urls.ts +36 -4
- package/src/work.ts +10 -0
- package/src/acquire.ts +0 -182
- package/src/host-text.ts +0 -92
- package/src/node.ts +0 -139
- package/src/store.ts +0 -189
package/src/ui/bootstrap.ts
CHANGED
|
@@ -17,13 +17,21 @@ import {
|
|
|
17
17
|
text as textNode,
|
|
18
18
|
} from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { configPath, normalizeNodeId, normalizeRegion, normalizeServiceUrl, saveConfig, shortNodeId } from "../config.ts";
|
|
20
|
+
import { parseAllowBlock, type AcquirePolicy } from "@onlyne/node-host/acquire";
|
|
21
|
+
import { ensureKeyDir } from "@onlyne/node-host/keys";
|
|
20
22
|
import { startRuntime } from "../runtime.ts";
|
|
21
|
-
import type { OnlyneConfig } from "
|
|
23
|
+
import type { OnlyneConfig } from "@onlyne/node-host/types";
|
|
22
24
|
import { t, type UiTextKey } from "./bootstrap-text.ts";
|
|
23
25
|
|
|
24
|
-
const FIELDS = ["region", "nodeId", "bind", "keyScope", "pageSize", "contact"] as const;
|
|
26
|
+
const FIELDS = ["region", "nodeId", "bind", "keyScope", "pageSize", "contact", "policy", "allow"] as const;
|
|
25
27
|
type FieldName = (typeof FIELDS)[number];
|
|
26
28
|
|
|
29
|
+
const POLICIES: ReadonlyArray<{ value: AcquirePolicy; key: UiTextKey }> = [
|
|
30
|
+
{ value: "all", key: "bootstrap.policy.all" },
|
|
31
|
+
{ value: "segment", key: "bootstrap.policy.segment" },
|
|
32
|
+
{ value: "none", key: "bootstrap.policy.none" },
|
|
33
|
+
];
|
|
34
|
+
|
|
27
35
|
const LABELS: Record<FieldName, UiTextKey> = {
|
|
28
36
|
region: "bootstrap.field.region",
|
|
29
37
|
nodeId: "bootstrap.field.nodeId",
|
|
@@ -31,6 +39,8 @@ const LABELS: Record<FieldName, UiTextKey> = {
|
|
|
31
39
|
keyScope: "bootstrap.field.keyScope",
|
|
32
40
|
pageSize: "bootstrap.field.pageSize",
|
|
33
41
|
contact: "bootstrap.field.contact",
|
|
42
|
+
policy: "bootstrap.field.policy",
|
|
43
|
+
allow: "bootstrap.field.allow",
|
|
34
44
|
};
|
|
35
45
|
|
|
36
46
|
const HINTS: Record<FieldName, UiTextKey> = {
|
|
@@ -40,6 +50,8 @@ const HINTS: Record<FieldName, UiTextKey> = {
|
|
|
40
50
|
keyScope: "bootstrap.hint.keyScope",
|
|
41
51
|
pageSize: "bootstrap.hint.pageSize",
|
|
42
52
|
contact: "bootstrap.hint.contact",
|
|
53
|
+
policy: "bootstrap.hint.policy",
|
|
54
|
+
allow: "bootstrap.hint.allow",
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
/** The values the command handler derived from Tern and the saved config. */
|
|
@@ -51,6 +63,10 @@ export interface BootstrapDefaults {
|
|
|
51
63
|
bind: string;
|
|
52
64
|
keyScope: string;
|
|
53
65
|
pageSize: number;
|
|
66
|
+
/** RFC 0018 §2: the policy this project currently enforces. */
|
|
67
|
+
policy: AcquirePolicy;
|
|
68
|
+
/** The allow set as stored, one entry per element. */
|
|
69
|
+
allow: readonly string[];
|
|
54
70
|
/** Existing configuration; the draft carries what the form does not edit. */
|
|
55
71
|
current?: OnlyneConfig;
|
|
56
72
|
}
|
|
@@ -96,6 +112,8 @@ export class OnlyneBootstrap implements Component {
|
|
|
96
112
|
(defaults.current?.discovery?.seeds ?? []).join(", "),
|
|
97
113
|
t(HINTS.contact),
|
|
98
114
|
),
|
|
115
|
+
policy: this.makeField(t(LABELS.policy), defaults.policy, t(HINTS.policy)),
|
|
116
|
+
allow: this.makeField(t(LABELS.allow), defaults.allow.join(", "), t(HINTS.allow)),
|
|
99
117
|
};
|
|
100
118
|
}
|
|
101
119
|
|
|
@@ -162,6 +180,21 @@ export class OnlyneBootstrap implements Component {
|
|
|
162
180
|
this.fail(error instanceof Error ? error.message : String(error));
|
|
163
181
|
return;
|
|
164
182
|
}
|
|
183
|
+
const policy = this.value("policy").toLowerCase();
|
|
184
|
+
if (policy !== "all" && policy !== "segment" && policy !== "none") {
|
|
185
|
+
this.fail(t("bootstrap.policyInvalid"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const allow = this.value("allow")
|
|
189
|
+
.split(/[,\s]+/)
|
|
190
|
+
.map(entry => entry.trim())
|
|
191
|
+
.filter(entry => entry.length > 0);
|
|
192
|
+
for (const entry of allow) {
|
|
193
|
+
if (parseAllowBlock(entry) === undefined) {
|
|
194
|
+
this.fail(t("bootstrap.allowInvalid", { value: entry }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
165
198
|
const previous = this.defaults.current;
|
|
166
199
|
const config: OnlyneConfig = {
|
|
167
200
|
path: configPath(this.ctx.cwd),
|
|
@@ -180,7 +213,7 @@ export class OnlyneBootstrap implements Component {
|
|
|
180
213
|
threads: { page_size: Number(this.value("pageSize")) },
|
|
181
214
|
...(seeds.length === 0 ? {} : { discovery: { seeds } }),
|
|
182
215
|
...(previous?.trust === undefined ? {} : { trust: previous.trust }),
|
|
183
|
-
|
|
216
|
+
acquisition: { policy, allow },
|
|
184
217
|
...(previous?.language === undefined ? {} : { language: previous.language }),
|
|
185
218
|
};
|
|
186
219
|
this.saving = true;
|
|
@@ -188,6 +221,14 @@ export class OnlyneBootstrap implements Component {
|
|
|
188
221
|
this.changed();
|
|
189
222
|
let persisted = false;
|
|
190
223
|
try {
|
|
224
|
+
if (scope !== "none") {
|
|
225
|
+
try {
|
|
226
|
+
await ensureKeyDir(this.ctx.cwd, config.node.id, scope);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.fail(error instanceof Error ? error.message : String(error));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
191
232
|
await saveConfig(config);
|
|
192
233
|
persisted = true;
|
|
193
234
|
const runtime = await startRuntime(this.pi, this.ctx);
|
|
@@ -254,6 +295,29 @@ export class OnlyneBootstrap implements Component {
|
|
|
254
295
|
return node("row", { role: "omp.btn", actions: { click: act }, title: label }, [textNode(label)], act);
|
|
255
296
|
}
|
|
256
297
|
|
|
298
|
+
/** The three values side by side: a policy is chosen, not typed into a free-form field. */
|
|
299
|
+
private policyRow(): NativeNode {
|
|
300
|
+
const current = this.value("policy").toLowerCase();
|
|
301
|
+
return node(
|
|
302
|
+
"row",
|
|
303
|
+
{ role: "onlyne.policy", gap: "sm", align: "center" },
|
|
304
|
+
POLICIES.map(item =>
|
|
305
|
+
node(
|
|
306
|
+
"row",
|
|
307
|
+
{
|
|
308
|
+
role: "omp.btn",
|
|
309
|
+
actions: { click: `onlyne.bootstrap.policy.${item.value}` },
|
|
310
|
+
tone: current === item.value ? "accent" : undefined,
|
|
311
|
+
title: t("bootstrap.policy.title", { value: item.value }),
|
|
312
|
+
},
|
|
313
|
+
[textNode([span(t(item.key), current === item.value ? "accent strong" : "muted")])],
|
|
314
|
+
item.value,
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
"policy-switch",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
257
321
|
describe(_cx: DescribeContext): NativeNode {
|
|
258
322
|
if (this.locale !== getLocale()) {
|
|
259
323
|
this.locale = getLocale();
|
|
@@ -268,6 +332,7 @@ export class OnlyneBootstrap implements Component {
|
|
|
268
332
|
body.push(draft.field);
|
|
269
333
|
} else body.push(this.fieldRow(name));
|
|
270
334
|
if (name === "region") body.push(node("text", { text: this.regionPreview(), wrap: "word" }));
|
|
335
|
+
if (name === "policy") body.push(this.policyRow());
|
|
271
336
|
}
|
|
272
337
|
if (this.error !== undefined) body.push(node("text", { spans: [span(this.error, "error")], wrap: "word" }));
|
|
273
338
|
body.push(
|
|
@@ -292,6 +357,15 @@ export class OnlyneBootstrap implements Component {
|
|
|
292
357
|
this.done();
|
|
293
358
|
return;
|
|
294
359
|
}
|
|
360
|
+
if (act.startsWith("onlyne.bootstrap.policy.")) {
|
|
361
|
+
const value = act.slice("onlyne.bootstrap.policy.".length);
|
|
362
|
+
const known = POLICIES.find(item => item.value === value);
|
|
363
|
+
if (known === undefined) return;
|
|
364
|
+
this.active = "policy";
|
|
365
|
+
this.fields.policy.editor.setText(known.value);
|
|
366
|
+
this.changed();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
295
369
|
if (act.startsWith("onlyne.bootstrap.field.")) {
|
|
296
370
|
const name = act.slice("onlyne.bootstrap.field.".length) as FieldName;
|
|
297
371
|
if (FIELDS.includes(name)) {
|
package/src/ui/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
2
|
import { ConfigError, loadConfig } from "../config.ts";
|
|
3
|
+
import { DEFAULT_ACQUISITION } from "@onlyne/node-host/acquire";
|
|
3
4
|
import { getRuntime, startRuntime } from "../runtime.ts";
|
|
4
5
|
import { detectTernSession } from "../tern.ts";
|
|
5
|
-
import type { OnlyneConfig } from "
|
|
6
|
+
import type { OnlyneConfig } from "@onlyne/node-host/types";
|
|
6
7
|
import { OnlyneBootstrap, type BootstrapDefaults } from "./bootstrap.ts";
|
|
7
8
|
import { OnlyneViewer } from "./viewer.ts";
|
|
8
9
|
import { LOCALES, setLocale, type Locale } from "@onlyne/node-host/i18n";
|
|
@@ -70,7 +71,8 @@ export function registerUi(pi: ExtensionAPI): void {
|
|
|
70
71
|
try {
|
|
71
72
|
let runtime = getRuntime();
|
|
72
73
|
if (command === "init") {
|
|
73
|
-
|
|
74
|
+
// Only refuse init if the runtime is actually running; if unstarted or down, allow init to proceed or retry bootstrap.
|
|
75
|
+
const existing = runtime?.config;
|
|
74
76
|
if (existing !== undefined) {
|
|
75
77
|
ctx.ui.notify(t("configured"), "info");
|
|
76
78
|
return;
|
|
@@ -103,6 +105,8 @@ export function registerUi(pi: ExtensionAPI): void {
|
|
|
103
105
|
bind: current?.server.bind ?? "127.0.0.1",
|
|
104
106
|
keyScope: current?.node.key_scope ?? "project",
|
|
105
107
|
pageSize: current?.threads.page_size ?? 50,
|
|
108
|
+
policy: current?.acquisition?.policy ?? DEFAULT_ACQUISITION.policy,
|
|
109
|
+
allow: current?.acquisition?.allow ?? DEFAULT_ACQUISITION.allow,
|
|
106
110
|
...(current === undefined ? {} : { current }),
|
|
107
111
|
};
|
|
108
112
|
await ctx.ui.custom<void>(
|
package/src/ui/mail-message.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { MessageRenderer } from "@oh-my-pi/pi-coding-agent";
|
|
2
2
|
import { getLocale } from "@onlyne/node-host/i18n";
|
|
3
3
|
import { Markdown, Text, getMarkdownTheme, node, span, text, type Component, type NativeNode, type Theme, type TspSpan } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import type { HistoryMessage, MessageRef, ThreadRef } from "@onlyne/lib";
|
|
4
|
+
import type { HistoryMessage, MessageRef, ThreadRef, WorkRef } from "@onlyne/lib";
|
|
5
5
|
import { shortNodeId } from "../config.ts";
|
|
6
6
|
import { getRuntime, type PluginRuntime } from "../runtime.ts";
|
|
7
7
|
import { conversationPath, messagePath } from "../urls.ts";
|
|
8
|
+
import { workValue } from "../work.ts";
|
|
8
9
|
import { t } from "./bootstrap-text.ts";
|
|
9
10
|
|
|
10
11
|
export interface OnlyneMessageDetails {
|
|
@@ -22,6 +23,7 @@ export interface OnlyneMessageDetails {
|
|
|
22
23
|
body: string;
|
|
23
24
|
relation?: "reply" | "followup";
|
|
24
25
|
in_reply_to?: MessageRef;
|
|
26
|
+
work?: WorkRef;
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -169,6 +171,7 @@ class MailMessage implements Component {
|
|
|
169
171
|
...(mail.in_reply_to === undefined
|
|
170
172
|
? []
|
|
171
173
|
: [`${this.theme.fg("muted", `${t("mail.inReplyTo")}:`)} ${mail.in_reply_to.sender}/${mail.in_reply_to.id}`]),
|
|
174
|
+
...(mail.work === undefined ? [] : [`${this.theme.fg("muted", `${t("mail.work")}:`)} ${workValue(mail.work)}`]),
|
|
172
175
|
`${this.theme.fg("muted", `${t("mail.delivery")}:`)} ${this.status.delivery}${
|
|
173
176
|
this.status.consumption === undefined ? "" : t("mail.consumed", { state: this.status.consumption })
|
|
174
177
|
}`,
|
|
@@ -215,6 +218,7 @@ class MailMessage implements Component {
|
|
|
215
218
|
k: t("mail.inReplyTo"),
|
|
216
219
|
v: [span(`${mail.in_reply_to.sender}/${mail.in_reply_to.id}`, "link mono", { href: replyHref })],
|
|
217
220
|
}]),
|
|
221
|
+
...(mail.work === undefined ? [] : [{ k: t("mail.work"), v: [span(workValue(mail.work), "mono")] }]),
|
|
218
222
|
{
|
|
219
223
|
k: t("mail.delivery"),
|
|
220
224
|
v: [
|
package/src/ui/viewer-text.ts
CHANGED
|
@@ -43,7 +43,6 @@ const en = {
|
|
|
43
43
|
"approvals.head": "{node} asks to exchange keys with you",
|
|
44
44
|
"field.peerFingerprint": "peer fingerprint",
|
|
45
45
|
"field.ownFingerprint": "local fingerprint",
|
|
46
|
-
"field.endpoint": "endpoint",
|
|
47
46
|
"action.accept": "Accept",
|
|
48
47
|
"approvals.accept.title": "Fingerprints match; approve this exchange",
|
|
49
48
|
"action.reject": "Reject",
|
|
@@ -51,7 +50,15 @@ const en = {
|
|
|
51
50
|
"action.details": "Details",
|
|
52
51
|
"approvals.details.title": "Open this node in the member column",
|
|
53
52
|
|
|
54
|
-
"message.
|
|
53
|
+
"message.open.title": "#{seq} · click to read the whole message",
|
|
54
|
+
"messageDetail.head": "#{seq} · {author} · {time}",
|
|
55
|
+
"messageDetail.close": "Back to messages",
|
|
56
|
+
"messageDetail.close.title": "Close the full message",
|
|
57
|
+
"messageDetail.reply": "Reply",
|
|
58
|
+
"messageDetail.reply.title": "Answer this message",
|
|
59
|
+
"messageDetail.target": "Use as recipient",
|
|
60
|
+
"messageDetail.target.title": "Start a new message to this author",
|
|
61
|
+
"messageDetail.work": "Work: {value}",
|
|
55
62
|
|
|
56
63
|
"composer.reply": "Replying to {node} · {id}",
|
|
57
64
|
"composer.new": "New message to {node}",
|
|
@@ -104,11 +111,14 @@ const en = {
|
|
|
104
111
|
"field.fingerprint": "fingerprint",
|
|
105
112
|
"field.domain": "domain",
|
|
106
113
|
"field.status": "status",
|
|
114
|
+
"field.declared": "declared address",
|
|
115
|
+
"field.observed": "observed address",
|
|
107
116
|
"field.source": "source",
|
|
108
117
|
"field.lastSeen": "last seen",
|
|
109
118
|
"member.fingerprint.none": "not exchanged",
|
|
110
119
|
"member.source.seed": "connected by address (saved as a seed)",
|
|
111
120
|
"member.lastSeen.stale": " · record expired",
|
|
121
|
+
"member.observed.none": "not recorded",
|
|
112
122
|
"action.useTarget": "Use as recipient",
|
|
113
123
|
"member.target.title": "Send messages to this node",
|
|
114
124
|
"action.acceptExchange": "Accept exchange",
|
|
@@ -118,6 +128,46 @@ const en = {
|
|
|
118
128
|
"verb.revoke": "Revoke",
|
|
119
129
|
"verb.reject": "Reject",
|
|
120
130
|
|
|
131
|
+
"trustpage.open": "Trust rules",
|
|
132
|
+
"trustpage.open.title": "Acquisition policy, pending peers and the decision record",
|
|
133
|
+
"trustpage.title": "Trust & acquisition",
|
|
134
|
+
"trustpage.meta": "{pending} waiting for you · {audit} recorded decisions",
|
|
135
|
+
"trustpage.policy": "acquisition policy",
|
|
136
|
+
"trustpage.policy.all": "all: any peer is approved automatically",
|
|
137
|
+
"trustpage.policy.segment": "segment: only the allow set is approved automatically",
|
|
138
|
+
"trustpage.policy.none": "none: only a human approves a peer",
|
|
139
|
+
"trustpage.policy.unknown": "this node has not reported a policy",
|
|
140
|
+
"trustpage.policy.set": "Set the acquisition policy to {value}",
|
|
141
|
+
"trustpage.allow": "allowed networks",
|
|
142
|
+
"trustpage.allow.none": "none: no peer is approved automatically",
|
|
143
|
+
"trustpage.allowLoopback": "loopback discovery",
|
|
144
|
+
"trustpage.allowLoopback.on": "on",
|
|
145
|
+
"trustpage.allowLoopback.off": "off (the default)",
|
|
146
|
+
"trustpage.edit": "Changes restart this node; trust already established is not touched.",
|
|
147
|
+
"trustpage.allow.edit": "Edit allow set",
|
|
148
|
+
"trustpage.allow.edit.title": "Append or remove network/prefix blocks and host names",
|
|
149
|
+
"trustpage.allow.save": "Save allow set",
|
|
150
|
+
"trustpage.allow.cancel": "Cancel",
|
|
151
|
+
"trustpage.allowInvalid": "Not a network/prefix block or host name: {value}",
|
|
152
|
+
"trustpage.warn.lan":
|
|
153
|
+
"A private or wide range lets every host inside it exchange keys with this node without asking you first. The set is matched against the address this node observed, not the address a peer declares.",
|
|
154
|
+
"trustpage.warn.axes":
|
|
155
|
+
"Discovery and acquisition are separate: discovery excludes loopback by default while acquisition approves only the allow set. Changing one does not change the other.",
|
|
156
|
+
"trustpage.warn.limits":
|
|
157
|
+
"Not covered: the real client address behind a reverse proxy, and DNS names that resolve to an address other than the one actually connected.",
|
|
158
|
+
"trustpage.invited.head": "waiting for your decision",
|
|
159
|
+
"trustpage.invited.empty": "No peer is waiting for approval.",
|
|
160
|
+
"trustpage.audit.head": "acquisition record",
|
|
161
|
+
"trustpage.audit.empty": "No acquisition decision was recorded in this session.",
|
|
162
|
+
"trustpage.actor.human": "human",
|
|
163
|
+
"trustpage.actor.agent": "agent",
|
|
164
|
+
"trustpage.actor.seed": "seed",
|
|
165
|
+
"trustpage.actor.policy": "policy",
|
|
166
|
+
"trustpage.override": "human override",
|
|
167
|
+
"trustpage.seed": "seed pre-authorization",
|
|
168
|
+
"trustpage.audit.allowed": "allowed",
|
|
169
|
+
"trustpage.audit.denied": "denied",
|
|
170
|
+
|
|
121
171
|
"notice.refreshing": "Refreshing discovery…",
|
|
122
172
|
"notice.refreshed": "Discovery refreshed.",
|
|
123
173
|
"notice.refreshFailed": "Refresh failed: {error}",
|
|
@@ -133,6 +183,10 @@ const en = {
|
|
|
133
183
|
"notice.requestFailed": "Request failed: {error}",
|
|
134
184
|
"notice.exchangeSent": "Key exchange with {node} submitted; both sides' approval shows in the member column.",
|
|
135
185
|
"notice.sendFailed": "Send failed: {error}",
|
|
186
|
+
"notice.policySaved": "Acquisition policy is now {policy}; the node restarted.",
|
|
187
|
+
"notice.policyFailed": "Policy change failed: {error}",
|
|
188
|
+
"notice.allowSaved": "Allow set saved; the node restarted.",
|
|
189
|
+
"notice.allowFailed": "Allow set failed: {error}",
|
|
136
190
|
|
|
137
191
|
"outcome.refused": "Not delivered: {error}",
|
|
138
192
|
"outcome.refused.peer": "the peer refused",
|
|
@@ -194,7 +248,6 @@ const zh: Record<ViewerTextKey, string> = {
|
|
|
194
248
|
"approvals.head": "{node} 请求与你交换密钥",
|
|
195
249
|
"field.peerFingerprint": "对方指纹",
|
|
196
250
|
"field.ownFingerprint": "本机指纹",
|
|
197
|
-
"field.endpoint": "端点",
|
|
198
251
|
"action.accept": "同意",
|
|
199
252
|
"approvals.accept.title": "双方指纹一致,批准这次交换",
|
|
200
253
|
"action.reject": "拒绝",
|
|
@@ -202,7 +255,15 @@ const zh: Record<ViewerTextKey, string> = {
|
|
|
202
255
|
"action.details": "查看详情",
|
|
203
256
|
"approvals.details.title": "在成员栏打开这位节点",
|
|
204
257
|
|
|
205
|
-
"message.
|
|
258
|
+
"message.open.title": "#{seq} · 点击读全文",
|
|
259
|
+
"messageDetail.head": "#{seq} · {author} · {time}",
|
|
260
|
+
"messageDetail.close": "返回消息列表",
|
|
261
|
+
"messageDetail.close.title": "关闭全文",
|
|
262
|
+
"messageDetail.reply": "回复",
|
|
263
|
+
"messageDetail.reply.title": "回复这一条",
|
|
264
|
+
"messageDetail.target": "用作发送目标",
|
|
265
|
+
"messageDetail.target.title": "给这位作者发一条新消息",
|
|
266
|
+
"messageDetail.work": "工作单元:{value}",
|
|
206
267
|
|
|
207
268
|
"composer.reply": "回复 {node} · {id}",
|
|
208
269
|
"composer.new": "新消息发送到 {node}",
|
|
@@ -255,11 +316,14 @@ const zh: Record<ViewerTextKey, string> = {
|
|
|
255
316
|
"field.fingerprint": "指纹",
|
|
256
317
|
"field.domain": "域",
|
|
257
318
|
"field.status": "状态",
|
|
319
|
+
"field.declared": "声明地址",
|
|
320
|
+
"field.observed": "观测地址",
|
|
258
321
|
"field.source": "来源",
|
|
259
322
|
"field.lastSeen": "最后可见",
|
|
260
323
|
"member.fingerprint.none": "未交换",
|
|
261
324
|
"member.source.seed": "按地址连接(已存为种子)",
|
|
262
325
|
"member.lastSeen.stale": " · 记录已过期",
|
|
326
|
+
"member.observed.none": "未记录",
|
|
263
327
|
"action.useTarget": "用作发送目标",
|
|
264
328
|
"member.target.title": "把消息发给这位节点",
|
|
265
329
|
"action.acceptExchange": "同意交换",
|
|
@@ -269,6 +333,46 @@ const zh: Record<ViewerTextKey, string> = {
|
|
|
269
333
|
"verb.revoke": "撤销",
|
|
270
334
|
"verb.reject": "拒绝",
|
|
271
335
|
|
|
336
|
+
"trustpage.open": "信任规则",
|
|
337
|
+
"trustpage.open.title": "获取策略、待批节点与判定记录",
|
|
338
|
+
"trustpage.title": "信任与获取",
|
|
339
|
+
"trustpage.meta": "{pending} 个待你确认 · {audit} 条判定记录",
|
|
340
|
+
"trustpage.policy": "获取策略",
|
|
341
|
+
"trustpage.policy.all": "all:任意对端都自动批准",
|
|
342
|
+
"trustpage.policy.segment": "segment:只自动批准允许集合",
|
|
343
|
+
"trustpage.policy.none": "none:只有人类能批准",
|
|
344
|
+
"trustpage.policy.unknown": "运行中的节点没有报告策略",
|
|
345
|
+
"trustpage.policy.set": "把获取策略设为 {value}",
|
|
346
|
+
"trustpage.allow": "允许集合",
|
|
347
|
+
"trustpage.allow.none": "空:不对任何对端自动批准",
|
|
348
|
+
"trustpage.allowLoopback": "回环发现",
|
|
349
|
+
"trustpage.allowLoopback.on": "开",
|
|
350
|
+
"trustpage.allowLoopback.off": "关(默认)",
|
|
351
|
+
"trustpage.edit": "修改会重启本节点;已经建立的信任不受影响。",
|
|
352
|
+
"trustpage.allow.edit": "修改允许集合",
|
|
353
|
+
"trustpage.allow.edit.title": "增删 network/prefix 网段与主机名",
|
|
354
|
+
"trustpage.allow.save": "保存允许集合",
|
|
355
|
+
"trustpage.allow.cancel": "取消",
|
|
356
|
+
"trustpage.allowInvalid": "不是 network/prefix 网段或主机名:{value}",
|
|
357
|
+
"trustpage.warn.lan":
|
|
358
|
+
"私有网段或过宽的网段会让其中每台主机都能不经你确认与本节点交换密钥。允许集合按本节点观测到的地址判定,不按对端声明地址。",
|
|
359
|
+
"trustpage.warn.axes":
|
|
360
|
+
"发现与获取是两条轴:发现默认排除回环,获取只批准允许集合。改一项不会自动改另一项。",
|
|
361
|
+
"trustpage.warn.limits":
|
|
362
|
+
"不覆盖:反向代理后的真实客户端地址;DNS 解析结果与实际连接目标不一致的情形。",
|
|
363
|
+
"trustpage.invited.head": "待你决定",
|
|
364
|
+
"trustpage.invited.empty": "没有待批准的节点。",
|
|
365
|
+
"trustpage.audit.head": "获取记录",
|
|
366
|
+
"trustpage.audit.empty": "本次会话还没有获取判定记录。",
|
|
367
|
+
"trustpage.actor.human": "人类",
|
|
368
|
+
"trustpage.actor.agent": "agent",
|
|
369
|
+
"trustpage.actor.seed": "种子",
|
|
370
|
+
"trustpage.actor.policy": "策略",
|
|
371
|
+
"trustpage.override": "人类超控",
|
|
372
|
+
"trustpage.seed": "种子预授权",
|
|
373
|
+
"trustpage.audit.allowed": "允许",
|
|
374
|
+
"trustpage.audit.denied": "拒绝",
|
|
375
|
+
|
|
272
376
|
"notice.refreshing": "正在刷新发现…",
|
|
273
377
|
"notice.refreshed": "发现已刷新。",
|
|
274
378
|
"notice.refreshFailed": "刷新失败:{error}",
|
|
@@ -284,6 +388,10 @@ const zh: Record<ViewerTextKey, string> = {
|
|
|
284
388
|
"notice.requestFailed": "请求失败:{error}",
|
|
285
389
|
"notice.exchangeSent": "已提交与 {node} 的密钥交换,双方批准状态见成员栏。",
|
|
286
390
|
"notice.sendFailed": "发送失败:{error}",
|
|
391
|
+
"notice.policySaved": "获取策略已改为 {policy},节点已重启。",
|
|
392
|
+
"notice.policyFailed": "修改策略失败:{error}",
|
|
393
|
+
"notice.allowSaved": "允许集合已保存,节点已重启。",
|
|
394
|
+
"notice.allowFailed": "修改允许集合失败:{error}",
|
|
287
395
|
|
|
288
396
|
"outcome.refused": "未送达:{error}",
|
|
289
397
|
"outcome.refused.peer": "对方拒绝",
|