ai_co_discuss 0.3.19 → 0.3.20
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/skills/discuss-participant/SKILL.md +4 -4
- package/.agents/skills/discuss-participant/references/discussion.md +2 -2
- package/.agents/skills/discuss-participant/references/writing.md +1 -1
- package/package.json +1 -1
- package/runner/discuss.py +71 -20
- package/runner/discussion_launch.mjs +1 -1
- package/runner/expert_prompts.mjs +3 -2
- package/runner/free_runner.mjs +1 -1
- package/web/app.js +3 -3
- package/web/index.html +15 -1
- package/web/modern.css +1 -1
- package/web/modern.js +2064 -2061
|
@@ -26,16 +26,16 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
26
26
|
|
|
27
27
|
1. **不读取或打印凭据内容**,token 不进入提示或房间。每个成员只用自己的凭据,AI 不借用用户身份发言,房主控制凭据不能代替成员署名。(身份与权限由网站按凭据校验。)
|
|
28
28
|
2. **执行失效后不再写入**:暂停、到期或被打断的旧任务保留草稿并结束,不换身份、职责或编号绕过拒绝,也不去掉 `--execution` 改作普通发言。任务文件的 `stopped_executions` 列出你此前被用户停止或跳过的执行:会话里如有它们未完成的工作,不接着做、不为它们补交,只处理本次请求。(失效的执行若还能产生可见结果,用户的打断与暂停就失去意义。)
|
|
29
|
-
3. **写入前定好 `--submission-id
|
|
29
|
+
3. **写入前定好 `--submission-id`**:同次重试沿用编号和原内容,新内容换新编号;`message post` 可省略,CLI 按房间、身份、关联与内容生成,用同一份草稿重试即得同一编号;结果不明时先按原编号查询,再按 [写入与恢复](references/writing.md) 处理。(网站靠它去重,换号会产生重复发言。)
|
|
30
30
|
4. **不创建定时任务、不操作其他软件、不发状态回执**(就绪、开始、完成、停止等说明),除非用户要求或出现需要记录的异常;回执不能作为观点、完成或共识的证据。(这些动作有副作用,回执只会淹没真正的回答。)
|
|
31
31
|
|
|
32
32
|
## 命令入口与身份
|
|
33
33
|
|
|
34
34
|
用统一命令 `codiscuss room …`、`codiscuss message …` 等;任务或用户显式指定 CLI 路径时沿用指定入口,未安装时用已核实的 `python 路径/runner/discuss.py …`(统一入口需 Node.js 24+,网站读写需 Python 3.10+)。裸 `codiscuss` 会启动服务,不要单独运行;用法查 `codiscuss --help`,字段以本技能为准,这里没写的才查 `codiscuss schema 具体命令`。下文命令均省略入口前缀。
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
从实际任务或用户指定值核实站点、房间和身份,不从当前网页或最近聊天猜测。任务标明 `access_env: "DISCUSS_ACCESS_FILE"` 时凭据已由进程绑定,`--access-file FILE` 可省略;否则用任务交付的 `access_file`。任务凭据已绑定站点、房间、身份与本次执行,命令省略房间、`--as`、`--execution` 和 `--submission-id`,由 CLI 从凭据和内容补齐,例如 `message post --body-file 草稿.md`;下文命令里的 `ROOM`、`--as`、执行编号只在没有绑定凭据(如手动接入未带凭据)时需要填写。项目与 cwd 有值时用于本次获准资料;CLI 路径只定位工具,不把 CLI 仓库当作讨论项目或扩大文件权限。
|
|
37
37
|
|
|
38
|
-
每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell
|
|
38
|
+
每次 shell 调用都是一次模型往返,手动会话里还可能要用户逐条确认,所以互不依赖的只读命令放进同一条 shell 命令依次执行。正文想好后,先用当前软件的文件写入工具把正文(和需要的摘要)写成草稿文件,再用一条只含 `codiscuss` 的命令以 `--body-file`(`--summary-file`)提交;失败重试仍用这份草稿。不要用 heredoc、`echo` 重定向或命令行字面量传长文本,也不要在命令里调用函数生成编号(如 `[guid]::NewGuid()`、`$(uuidgen)`):自动运行时这类组合命令会被权限检查拒绝,文本中的引号(含中文弯引号)还会截断参数。其他写入命令需要编号时直接写一个新的 UUID 字面值。不同的网站写入各自单独执行并核对结果。
|
|
39
39
|
|
|
40
40
|
## 阅读
|
|
41
41
|
|
|
@@ -58,6 +58,6 @@ description: "AI CoDiscuss 的通用参与技能,所有参与者(包括主
|
|
|
58
58
|
|
|
59
59
|
## 保存与核验
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
正文用 UTF-8 `--body-file`,摘要含引号或较长时用 `--summary-file`。需要附件时先 `attachment upload ROOM --file FILE --submission-id 上传编号`,再用 `message post --attachment ID` 关联(最多 10 个,单个 20 MiB,须在执行资格结束前上传)。
|
|
62
62
|
|
|
63
63
|
成功须核对服务端返回的持久记录、作者和关联;返回已是持久记录时不再读一遍,缺少所需证据才按编号补读。答案落库后即可结束,不用旧资格继续写入。代发保留真实作者、完整原文及引用关系,摘要归实际撰写者,不把自己的改写冒充对方回答。执行结束不等于整个房间的工作结束。
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
自动任务用本次执行编号保存一次完整回答,例如:
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
|
-
codiscuss message post ROOM --as 本人 --kind chat --execution 执行编号 --body-file draft.md --
|
|
32
|
+
codiscuss message post ROOM --as 本人 --kind chat --execution 执行编号 --body-file draft.md --access-file FILE
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
手动成员用自己的成员凭据和服务端给出的请求编号:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
codiscuss message post ROOM --as 本人 --kind chat --request 请求编号 --body-file draft.md --
|
|
38
|
+
codiscuss message post ROOM --as 本人 --kind chat --request 请求编号 --body-file draft.md --access-file FILE
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
需要摘要时一并使用 `--summary` 或 `--summary-file`。成功提交后当前请求即完成;不要再补一条普通 chat 或状态回执。
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
主持类写入需要本次任务授予主持职责。网页成员代发须保留真实作者、完整原文、关联请求、原答复的链接/引用对应关系及同房间关联,不把自己的改写冒充他人原文。主持人补充的引用附录需明确标注来源与归属,无法取得的出处说明缺失;不把自己的核验结论混进专家原文。
|
|
30
30
|
|
|
31
|
-
房间、消息和安排创建使用显式 `--submission-id`:新内容生成新编号,同一次提交保留编号、原始资料、身份和关联。**先保存编号与原文,再发请求**,避免中断后无法核对。编号为 8–100 位字母、数字、下划线或连字符,可用 UUID
|
|
31
|
+
房间、消息和安排创建使用显式 `--submission-id`:新内容生成新编号,同一次提交保留编号、原始资料、身份和关联。**先保存编号与原文,再发请求**,避免中断后无法核对。编号为 8–100 位字母、数字、下划线或连字符,可用 UUID。`message post` 省略编号时由 CLI 按房间、身份、关联与内容生成(`auto-` 开头),草稿文件即原文记录,重试只需用同一份草稿。建房重试对照首次创建资料,不使用后来修改的话题内容替换请求。
|
|
32
32
|
|
|
33
33
|
长正文优先 UTF-8 文件,`--body-file -` 可读 stdin。当前消息正文上限为 32000 个 Unicode 字符,详细讨论状态上限为 4000 字;以目标站点协议为准。完整回答一次提交并完成安排,不能截断或用第一段冒充完整回答。超过上限时保留完整原文并说明限制,先明确完整材料的留档方式。
|
|
34
34
|
|
package/package.json
CHANGED
package/runner/discuss.py
CHANGED
|
@@ -54,12 +54,20 @@ class CliError(Exception):
|
|
|
54
54
|
self.details = details
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
# Shells split a literal text argument when it holds quotes they treat as delimiters; Windows PowerShell also treats
|
|
58
|
+
# Chinese curly quotes as quotes, which surfaces here as leftover arguments.
|
|
59
|
+
UNRECOGNIZED_HINT = "正文或摘要可能被 shell 按引号或空格截断(Windows PowerShell 也把中文弯引号当引号):改用 --body-file / --summary-file 传文本。"
|
|
60
|
+
|
|
61
|
+
|
|
57
62
|
class Parser(argparse.ArgumentParser):
|
|
58
63
|
"""Report parser failures through the same stderr contract as API errors."""
|
|
59
64
|
|
|
60
65
|
def error(self, message):
|
|
61
66
|
"""Raise a usage error instead of printing a separate argparse format."""
|
|
62
|
-
|
|
67
|
+
hint = f"查看 {self.prog} --help"
|
|
68
|
+
if message.startswith("unrecognized arguments"):
|
|
69
|
+
hint = UNRECOGNIZED_HINT + hint
|
|
70
|
+
raise CliError(message, hint=hint)
|
|
63
71
|
|
|
64
72
|
|
|
65
73
|
class NoRedirect(HTTPRedirectHandler):
|
|
@@ -131,6 +139,20 @@ def submission_id(value):
|
|
|
131
139
|
return value
|
|
132
140
|
|
|
133
141
|
|
|
142
|
+
def fill_submission_id(args, plan):
|
|
143
|
+
"""Derive an omitted submission ID from the command and its content, so resending the same content is deduplicated
|
|
144
|
+
without the caller generating an ID in the shell; the hosting session ID is left out because it does not change what is written."""
|
|
145
|
+
target = next((part for part in (plan.body, plan.params) if isinstance(part, dict) and "submission_id" in part), None)
|
|
146
|
+
if plan.method != "POST" or target is None or target["submission_id"]:
|
|
147
|
+
return
|
|
148
|
+
body = {key: value for key, value in (plan.body or {}).items() if key not in ("session_id", "session_client")}
|
|
149
|
+
material = {"operation": args.operation, "room": getattr(args, "room", None), "body": body, "params": plan.params}
|
|
150
|
+
if plan.upload_file:
|
|
151
|
+
material["file"] = hashlib.sha256(Path(plan.upload_file).read_bytes()).hexdigest()
|
|
152
|
+
digest = hashlib.sha256(json.dumps(material, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
|
|
153
|
+
target["submission_id"] = "auto-" + digest[:40]
|
|
154
|
+
|
|
155
|
+
|
|
134
156
|
def attachment_id(value):
|
|
135
157
|
"""Accept only opaque file identifiers, never local or remote paths."""
|
|
136
158
|
if not re.fullmatch(r"[a-f0-9]{32}", value):
|
|
@@ -143,7 +165,7 @@ def add_common(parser):
|
|
|
143
165
|
options = {"default": argparse.SUPPRESS}
|
|
144
166
|
parser.add_argument("--site", help="HTTP 服务地址;未指定时读 DISCUSS_URL,再读凭据中的 site", **options)
|
|
145
167
|
parser.add_argument("--as", dest="actor", type=participant,
|
|
146
|
-
help="
|
|
168
|
+
help="业务写入的声明操作者;省略时用凭据绑定的成员;不是登录认证;api 使用原始请求体", **options)
|
|
147
169
|
parser.add_argument("-o", "--output", choices=("json", "text"),
|
|
148
170
|
help="默认 json;成功 stdout,错误 stderr", **options)
|
|
149
171
|
parser.add_argument("--timeout", type=float, help="请求超时秒数,默认 20", **options)
|
|
@@ -180,7 +202,7 @@ def add_leaf(subcommands, registry, name, description, needs_room=False):
|
|
|
180
202
|
add_common(parser)
|
|
181
203
|
parser.set_defaults(operation=name)
|
|
182
204
|
if needs_room:
|
|
183
|
-
parser.add_argument("room", type=room_id, help="房间 ID
|
|
205
|
+
parser.add_argument("room", type=room_id, nargs="?", help="房间 ID;省略时用凭据绑定的房间")
|
|
184
206
|
registry[name] = parser
|
|
185
207
|
return parser
|
|
186
208
|
|
|
@@ -203,8 +225,8 @@ def room_commands(subcommands, registry):
|
|
|
203
225
|
create = add_leaf(subcommands, registry, "room create", "创建话题;不自动启动讨论或监控")
|
|
204
226
|
create.add_argument("--folder-id", help="直接在指定文件夹中创建;省略则未归类")
|
|
205
227
|
add_topic(create, create=True)
|
|
206
|
-
create.add_argument("--submission-id", type=submission_id,
|
|
207
|
-
help="
|
|
228
|
+
create.add_argument("--submission-id", type=submission_id,
|
|
229
|
+
help="新话题唯一编号;未知结果先查询,重试沿用编号和原始资料。省略时按命令与内容自动生成,同样内容重试得到同一编号")
|
|
208
230
|
update = add_leaf(subcommands, registry, "room update", "维护话题;省略的字段保持不变", True)
|
|
209
231
|
add_topic(update)
|
|
210
232
|
|
|
@@ -234,7 +256,7 @@ def message_commands(subcommands, registry):
|
|
|
234
256
|
post.add_argument("--summary-by", type=participant, help="摘要撰写者:原作者或实际操作者;默认操作者")
|
|
235
257
|
post.add_argument("--author", type=participant, help="原作者;默认等于 --as;不同即代发")
|
|
236
258
|
post.add_argument("--kind", choices=("chat", "moderation"), default="chat")
|
|
237
|
-
post.add_argument("--execution", type=positive_int, help="
|
|
259
|
+
post.add_argument("--execution", type=positive_int, help="自由房间:本次执行编号;省略时用凭据绑定的执行。执行失效后不能改作普通发言")
|
|
238
260
|
post.add_argument("--to", type=ai_name, action="append", default=[],
|
|
239
261
|
help="自由房间:主持人点名派活,普通成员提出回应建议(须房主确认);可重复")
|
|
240
262
|
post.add_argument("--handoff", action="store_true",
|
|
@@ -245,8 +267,8 @@ def message_commands(subcommands, registry):
|
|
|
245
267
|
post.add_argument("--request", type=positive_int, action="append", default=[],
|
|
246
268
|
help="自由房间手动接入成员:本次回答完成的请求编号,可重复;不能与 --execution 同用")
|
|
247
269
|
post.add_argument("--reply-to", type=positive_int, help="被引用的原文编号")
|
|
248
|
-
post.add_argument("--submission-id", type=submission_id,
|
|
249
|
-
help="
|
|
270
|
+
post.add_argument("--submission-id", type=submission_id,
|
|
271
|
+
help="每次新提交的唯一编号;重试必须沿用同编号和正文。省略时按房间、身份、关联与内容自动生成,同一份草稿重试得到同一编号")
|
|
250
272
|
|
|
251
273
|
|
|
252
274
|
def attachment_commands(subcommands, registry):
|
|
@@ -259,7 +281,7 @@ def attachment_commands(subcommands, registry):
|
|
|
259
281
|
view.add_argument("id", type=attachment_id)
|
|
260
282
|
upload = add_leaf(subcommands, registry, "attachment upload", "上传原文件;之后用 message post --attachment 关联消息", True)
|
|
261
283
|
upload.add_argument("--file", required=True, help="本地原文件路径;最多 20 MiB")
|
|
262
|
-
upload.add_argument("--submission-id", type=submission_id,
|
|
284
|
+
upload.add_argument("--submission-id", type=submission_id, help="上传唯一编号;省略时按命令与内容自动生成,同样内容重试得到同一编号")
|
|
263
285
|
download = add_leaf(subcommands, registry, "attachment download", "下载原文件到显式路径;不覆盖已有文件", True)
|
|
264
286
|
download.add_argument("id", type=attachment_id)
|
|
265
287
|
download.add_argument("--dest", required=True, help="保存文件的完整或相对路径;父目录须已存在")
|
|
@@ -288,8 +310,8 @@ def turn_commands(subcommands, registry):
|
|
|
288
310
|
add_text(parser, "question")
|
|
289
311
|
parser.add_argument("--plan-file", help='整轮安排 JSON 数组 [{"expert":"Claude","question":"…"}],按顺序接力;- 从标准输入读取')
|
|
290
312
|
parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;任何新安排前必读,专家状态变化后需重新获取")
|
|
291
|
-
parser.add_argument("--submission-id", type=submission_id,
|
|
292
|
-
help="
|
|
313
|
+
parser.add_argument("--submission-id", type=submission_id,
|
|
314
|
+
help="新安排唯一编号;重试沿用同编号和内容。省略时按命令与内容自动生成,同样内容重试得到同一编号")
|
|
293
315
|
if action == "reorder":
|
|
294
316
|
parser.add_argument("--revision", required=True, help="turn order 返回的 revision;顺序变化后需重新获取")
|
|
295
317
|
parser.add_argument("--member", action="append", type=ai_name, required=True,
|
|
@@ -297,7 +319,7 @@ def turn_commands(subcommands, registry):
|
|
|
297
319
|
if action == "ask":
|
|
298
320
|
parser.add_argument("--request", type=positive_int, required=True, help="turn roster 中 host_requests 的请求编号")
|
|
299
321
|
parser.add_argument("--expert", type=ai_name, required=True, help="在场且已注册的目标成员,含已发言者和主持人")
|
|
300
|
-
parser.add_argument("--submission-id", type=submission_id,
|
|
322
|
+
parser.add_argument("--submission-id", type=submission_id, help="转交唯一编号;重试沿用原编号与内容。省略时按命令与内容自动生成,同样内容重试得到同一编号")
|
|
301
323
|
if action in ("continue", "ask"):
|
|
302
324
|
parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;状态变化后须重新读取")
|
|
303
325
|
|
|
@@ -334,7 +356,7 @@ def execution_commands(subcommands, registry):
|
|
|
334
356
|
"""Read an execution or report its failure through its durable identifier."""
|
|
335
357
|
for action in ("view", "fail"):
|
|
336
358
|
parser = add_leaf(subcommands, registry, f"execution {action}", "读取执行或报告本次执行失败", True)
|
|
337
|
-
parser.add_argument("id", type=positive_int)
|
|
359
|
+
parser.add_argument("id", type=positive_int, nargs="?", help="执行编号;省略时用凭据绑定的执行")
|
|
338
360
|
if action == "fail":
|
|
339
361
|
parser.add_argument("--reason", required=True)
|
|
340
362
|
|
|
@@ -354,13 +376,13 @@ def review_commands(subcommands, registry):
|
|
|
354
376
|
advance = add_leaf(subcommands, registry, "review advance", "先读 review ready,再带 revision 只推进既定评审并处理对应交接", True)
|
|
355
377
|
advance.add_argument("--batch", type=positive_int, required=True)
|
|
356
378
|
advance.add_argument("--revision", required=True)
|
|
357
|
-
advance.add_argument("--submission-id", type=submission_id,
|
|
379
|
+
advance.add_argument("--submission-id", type=submission_id, help="推进唯一编号;省略时按命令与内容自动生成,同样内容重试得到同一编号")
|
|
358
380
|
|
|
359
381
|
|
|
360
382
|
def build_parser(prog="discuss"):
|
|
361
383
|
"""Build help and schema from the same command definitions."""
|
|
362
384
|
parser = Parser(prog=prog, description="AI CoDiscuss CLI:通过 HTTP 操作现有讨论网站。",
|
|
363
|
-
allow_abbrev=False, epilog="
|
|
385
|
+
allow_abbrev=False, epilog="站点、房间、操作者与执行编号省略时取自凭据文件;提交编号省略时按内容生成;不保存当前房间,不自动重试写入。")
|
|
364
386
|
add_common(parser)
|
|
365
387
|
parser.add_argument("--version", action="version", version=f"{prog} {VERSION}")
|
|
366
388
|
subcommands = parser.add_subparsers(dest="resource", required=True)
|
|
@@ -430,13 +452,32 @@ def text_field(args, field):
|
|
|
430
452
|
|
|
431
453
|
|
|
432
454
|
def require_actor(args):
|
|
433
|
-
"""
|
|
434
|
-
actor = getattr(args, "actor", None)
|
|
455
|
+
"""Use the declared operator, or the member a task credential is bound to."""
|
|
456
|
+
actor = getattr(args, "actor", None) or load_access(args).get("member")
|
|
435
457
|
if not actor:
|
|
436
|
-
raise CliError("
|
|
458
|
+
raise CliError("写入须用 --as 指定操作者,或使用绑定成员的凭据;该字段声明操作者,不提供身份认证")
|
|
437
459
|
return actor
|
|
438
460
|
|
|
439
461
|
|
|
462
|
+
def bound_execution(args):
|
|
463
|
+
"""Return the execution a task credential is bound to, if any."""
|
|
464
|
+
value = load_access(args).get("execution_id")
|
|
465
|
+
if value is None:
|
|
466
|
+
return None
|
|
467
|
+
try:
|
|
468
|
+
return positive_int(str(value))
|
|
469
|
+
except argparse.ArgumentTypeError as error:
|
|
470
|
+
raise CliError("凭据文件中的 execution_id 无效") from error
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def bind_room(args):
|
|
474
|
+
"""Fill an omitted room from the credential; a credential for another room is still rejected before sending."""
|
|
475
|
+
if hasattr(args, "room") and args.room is None:
|
|
476
|
+
args.room = load_access(args).get("room") or None
|
|
477
|
+
if not args.room:
|
|
478
|
+
raise CliError("请指定房间 ID,或使用绑定了房间的凭据(--access-file / DISCUSS_ACCESS_FILE)")
|
|
479
|
+
|
|
480
|
+
|
|
440
481
|
def topic_payload(args):
|
|
441
482
|
"""Collect only supplied topic fields; expert configuration is a full map."""
|
|
442
483
|
payload = {}
|
|
@@ -519,6 +560,8 @@ def plan_message(args):
|
|
|
519
560
|
**read_context_params(args)}
|
|
520
561
|
return RequestPlan(base, params={key: value for key, value in params.items() if value is not None})
|
|
521
562
|
actor = require_actor(args)
|
|
563
|
+
if not args.execution and not args.request:
|
|
564
|
+
args.execution = bound_execution(args)
|
|
522
565
|
if (args.to or args.handoff) and args.kind != "chat":
|
|
523
566
|
raise CliError("--to / --handoff 仅用于 chat 消息")
|
|
524
567
|
if args.kind == "moderation" and not (args.execution or args.request):
|
|
@@ -686,7 +729,10 @@ def plan_run(args):
|
|
|
686
729
|
|
|
687
730
|
def plan_execution(args):
|
|
688
731
|
"""Read or fail one durable member execution."""
|
|
689
|
-
|
|
732
|
+
execution_id = args.id or bound_execution(args)
|
|
733
|
+
if not execution_id:
|
|
734
|
+
raise CliError("请指定执行编号,或使用绑定了执行的凭据")
|
|
735
|
+
path = f"/api/rooms/{args.room}/executions/{execution_id}"
|
|
690
736
|
if args.action == "view":
|
|
691
737
|
return RequestPlan(path)
|
|
692
738
|
return RequestPlan(path, "POST", {"action": "fail", "reason": args.reason})
|
|
@@ -721,7 +767,9 @@ def build_plan(args):
|
|
|
721
767
|
"attachment": plan_attachment,
|
|
722
768
|
"state": plan_state, "api": plan_api,
|
|
723
769
|
"run": plan_run, "execution": plan_execution, "member": plan_member, "review": plan_review}
|
|
770
|
+
bind_room(args)
|
|
724
771
|
plan = RequestPlan("/api/health") if args.resource == "health" else handlers[args.resource](args)
|
|
772
|
+
fill_submission_id(args, plan)
|
|
725
773
|
if getattr(args, "dry_run", False) and plan.method != "POST" and plan.download_file is None:
|
|
726
774
|
raise CliError("--dry-run 仅用于写入命令;读取命令本身不会修改数据")
|
|
727
775
|
if plan.body is not None and len(json.dumps(plan.body, ensure_ascii=False).encode("utf-8")) > MAX_BODY_BYTES:
|
|
@@ -933,6 +981,9 @@ def prepare_creation_access(args, site, plan):
|
|
|
933
981
|
raise CliError("建房须用 --access-file 指定私有凭据文件路径;首次自动生成,重试沿用同一文件")
|
|
934
982
|
path = Path(args.access_file)
|
|
935
983
|
payload = plan.body
|
|
984
|
+
if path.exists() and not args.submission_id:
|
|
985
|
+
# A retry that omits the ID continues the creation this credential file already recorded.
|
|
986
|
+
payload["submission_id"] = read_object(path).get("submission_id") or payload["submission_id"]
|
|
936
987
|
access = read_object(path) if path.exists() else {"site": site, "room": "", "token": secrets.token_urlsafe(48),
|
|
937
988
|
"submission_id": payload.get("submission_id"), "members": {}}
|
|
938
989
|
if access.get("site") != site or access.get("submission_id") != payload.get("submission_id"):
|
|
@@ -959,7 +1010,7 @@ def finish_creation_access(args, site, plan, value):
|
|
|
959
1010
|
if member["access"] == "online":
|
|
960
1011
|
continue
|
|
961
1012
|
member_path = path.with_name(f"{path.stem}.{member['name']}.json")
|
|
962
|
-
save_access(member_path, {"site": site, "room": value["id"], "token": member["token"]})
|
|
1013
|
+
save_access(member_path, {"site": site, "room": value["id"], "member": member["name"], "token": member["token"]})
|
|
963
1014
|
paths[member["name"]] = str(member_path)
|
|
964
1015
|
return {"owner_access_file": str(path), "member_access_files": paths}
|
|
965
1016
|
|
|
@@ -562,7 +562,7 @@ async function member_room_context(home, raw) {
|
|
|
562
562
|
/** Write a manual member's private credential next to the room configuration, never into a receipt. */
|
|
563
563
|
function manual_access(path, input, name, token) {
|
|
564
564
|
const access_path = join(dirname(path), `manual-${name}.access.json`);
|
|
565
|
-
atomic_json(access_path, { site: input.site, room: input.room_id, token });
|
|
565
|
+
atomic_json(access_path, { site: input.site, room: input.room_id, member: name, token });
|
|
566
566
|
return access_path;
|
|
567
567
|
}
|
|
568
568
|
|
|
@@ -110,7 +110,7 @@ export const READ_RULE = '读含中文的文件用 rg --encoding utf-8 . 文件
|
|
|
110
110
|
* (messages, read point, execution, role, credentials), the skills it lists say how to use them, and `instructions`
|
|
111
111
|
* holds only the limits no skill can know. `access_file` is set where the credential is not bound to the process.
|
|
112
112
|
*/
|
|
113
|
-
export function free_task_text({ name, discussion, discussion_request, profile, cli,
|
|
113
|
+
export function free_task_text({ name, discussion, discussion_request, profile, cli, messages, read_after, task_path, access_file = '', read_rule = '' }) {
|
|
114
114
|
const scope = discussion ? `讨论进行中,用户直接向你提问(${messages}):只回答这个提问,不改变讨论安排和进度。` : '';
|
|
115
115
|
const commands = runs_commands(profile) ? '' : `命令只用 subcommands 列出的 ${cli} 子命令。`;
|
|
116
116
|
const instructions = `${software_rules(profile, cli)}${scope}${project_rule(profile, '按请求')}${commands}草稿写在 draft_directory。`;
|
|
@@ -122,7 +122,8 @@ export function free_task_text({ name, discussion, discussion_request, profile,
|
|
|
122
122
|
// A greeting deserves a quick answer, and reading the skills first costs it one or two model rounds.
|
|
123
123
|
const access = access_file ? ` --access-file ${access_file}` : '';
|
|
124
124
|
// A one-line answer goes inline: a draft file guards long or quoted text and retries, which a greeting does not need.
|
|
125
|
-
|
|
125
|
+
// The credential already names the room, the member and this execution, so the commands carry only what changes.
|
|
126
|
+
const quick = `问候、致谢等简单消息可跳过技能:${cli} message list --after ${read_after}${access} 看原文,${cli} message post --body "回答"${access} 回复(正文含引号时先写草稿再用 --body-file)。`;
|
|
126
127
|
const prompt = `用户在${discussion ? '讨论' : '自由协作'}房间点名了你(${name}),对应消息 ${messages}。${quick}其他请求先读任务文件 ${task_path},按其中列出的技能处理并提交一次回答。${read_rule}`;
|
|
127
128
|
return { instructions, prompt };
|
|
128
129
|
}
|
package/runner/free_runner.mjs
CHANGED
|
@@ -85,7 +85,7 @@ export function free_task_files(config, member, execution, binding, { session =
|
|
|
85
85
|
const access_path = join(config.directory, `${id}.access.json`);
|
|
86
86
|
const task_path = join(config.directory, `${id}.task.json`);
|
|
87
87
|
const profile = config.participants[member.name];
|
|
88
|
-
atomic_json(access_path, { site: config.site, room: config.room, token: config.members[member.name].token,
|
|
88
|
+
atomic_json(access_path, { site: config.site, room: config.room, member: member.name, token: config.members[member.name].token,
|
|
89
89
|
session_id: binding.session_id || '', execution_id: execution.id });
|
|
90
90
|
const adapter = adapter_of(profile.adapter);
|
|
91
91
|
const cli = process.platform === 'win32' ? adapter?.windows_command || 'codiscuss.cmd' : 'codiscuss';
|
package/web/app.js
CHANGED
|
@@ -2703,7 +2703,7 @@ function download_member_access(name) {
|
|
|
2703
2703
|
const room = state.room, access = access_snapshot();
|
|
2704
2704
|
const member = access.members.find(item => item.name === name);
|
|
2705
2705
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可下载的私有配置");
|
|
2706
|
-
download_private_json({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }, `member-${room.id}-${name}.json`);
|
|
2706
|
+
download_private_json({ site: location.origin, room: room.id, member: name, token: member_credentials(room.id)[name].token }, `member-${room.id}-${name}.json`);
|
|
2707
2707
|
notify("info", `已下载 ${name} 的私有配置`);
|
|
2708
2708
|
}
|
|
2709
2709
|
|
|
@@ -2712,7 +2712,7 @@ async function copy_member_access_text(name) {
|
|
|
2712
2712
|
const room = state.room, access = access_snapshot();
|
|
2713
2713
|
const member = access.members.find(item => item.name === name);
|
|
2714
2714
|
if (!access.can_manage || !member?.available || member.access === "online") throw new Error("当前浏览器没有此成员可复制的凭证");
|
|
2715
|
-
await copy_text(JSON.stringify({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token }));
|
|
2715
|
+
await copy_text(JSON.stringify({ site: location.origin, room: room.id, member: name, token: member_credentials(room.id)[name].token }));
|
|
2716
2716
|
notify("info", `已复制 ${name} 的凭证文本(含 token)`);
|
|
2717
2717
|
}
|
|
2718
2718
|
|
|
@@ -2731,7 +2731,7 @@ async function copy_member_join(name, with_credential = false) {
|
|
|
2731
2731
|
else if (with_credential) instruction = `${skills}凭证文本在下方:每条 codiscuss 命令都在同一命令内把它原样设为环境变量 DISCUSS_ACCESS(不回显;首条 room view 也要带,否则无法识别你),命令省略 --access-file;先执行 codiscuss room view ${room.id} --site ${location.origin} --as ${name},随后立即执行 codiscuss member register ${room.id} --site ${location.origin} --as ${name} 完成注册(只需一次,会话 ID 由程序自动附带,不要自己填写),之后命令保持本站点、房间和身份。模型与思考强度在原软件中设置。${MANUAL_TRIGGER_INSTRUCTION}`;
|
|
2732
2732
|
else instruction = `${skills}使用属于你的私有文件 member-${room.id}-${name}.json,由用户提供它在本机的实际完整路径;先执行 codiscuss room view ${room.id} --site ${location.origin} --as ${name} --access-file "实际完整路径";随后立即执行 codiscuss member register ${room.id} --site ${location.origin} --as ${name} --access-file "实际完整路径" 完成注册(只需一次,会话 ID 由程序自动附带,不要自己填写),之后命令保持本站点、房间和身份。不要读取或公开文件中的 token;若用户给的是凭证文本而非文件,把它设为环境变量 DISCUSS_ACCESS(不要回显)并省略 --access-file。模型与思考强度在原软件中设置。${MANUAL_TRIGGER_INSTRUCTION}`;
|
|
2733
2733
|
const credential = with_credential ? `
|
|
2734
|
-
凭证文本(含 token,仅限你使用):${JSON.stringify({ site: location.origin, room: room.id, token: member_credentials(room.id)[name].token })}` : "";
|
|
2734
|
+
凭证文本(含 token,仅限你使用):${JSON.stringify({ site: location.origin, room: room.id, member: name, token: member_credentials(room.id)[name].token })}` : "";
|
|
2735
2735
|
await copy_text(`请以 ${name} 身份参与房间。
|
|
2736
2736
|
${instruction}
|
|
2737
2737
|
房间入口:${address}
|
package/web/index.html
CHANGED
|
@@ -4,7 +4,21 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<!-- Tablets lay out the desktop page at the desktop width and scale it to fit, so cards and panels match the PC exactly. -->
|
|
7
|
-
|
|
7
|
+
<!-- iOS Safari zooms into inputs whose text renders below 16px; capping the zoom at the fitted scale stops that, and iOS still allows pinch zoom. Android keeps the plain viewport so pinch zoom stays available there. -->
|
|
8
|
+
<script>(function () {
|
|
9
|
+
var meta = document.querySelector('meta[name="viewport"]');
|
|
10
|
+
var tablet = matchMedia("(pointer: coarse)").matches && Math.min(screen.width, screen.height) >= 600;
|
|
11
|
+
var ios = /iP(hone|ad|od)/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
12
|
+
function apply() {
|
|
13
|
+
if (!ios) { if (tablet) meta.content = "width=1440"; return; }
|
|
14
|
+
if (!tablet) { meta.content = "width=device-width, initial-scale=1, maximum-scale=1"; return; }
|
|
15
|
+
var window_width = window.visualViewport ? visualViewport.width * visualViewport.scale : innerWidth;
|
|
16
|
+
var scale = Math.round(window_width / 1440 * 10000) / 10000;
|
|
17
|
+
meta.content = "width=1440, initial-scale=" + scale + ", maximum-scale=" + scale;
|
|
18
|
+
}
|
|
19
|
+
apply();
|
|
20
|
+
if (ios && tablet) addEventListener("resize", apply);
|
|
21
|
+
})();</script>
|
|
8
22
|
<meta name="description" content="共议聊天室,让不同 AI 与人围绕同一份记录共同讨论。">
|
|
9
23
|
<title>共议聊天室 · AI CoDiscuss</title>
|
|
10
24
|
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgcng9IjIyIiBmaWxsPSIjMTUxOTFhIi8+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQgMTQpIHNjYWxlKC43MikiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iZjAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNC41IiB5MT0iNjQiIHgyPSI5NS41IiB5Mj0iNjQiPjxzdG9wIG9mZnNldD0iMC4wJSIgc3RvcC1jb2xvcj0iI2U5ZWJlZCIvPjxzdG9wIG9mZnNldD0iNi4zJSIgc3RvcC1jb2xvcj0iI2UyZTVlNyIvPjxzdG9wIG9mZnNldD0iMTIuNSUiIHN0b3AtY29sb3I9IiNkY2RlZTEiLz48c3RvcCBvZmZzZXQ9IjE4LjglIiBzdG9wLWNvbG9yPSIjZDVkOGRiIi8+PHN0b3Agb2Zmc2V0PSIyNS4wJSIgc3RvcC1jb2xvcj0iI2NlZDJkNSIvPjxzdG9wIG9mZnNldD0iMzEuMyUiIHN0b3AtY29sb3I9IiNjOGNiY2YiLz48c3RvcCBvZmZzZXQ9IjM3LjUlIiBzdG9wLWNvbG9yPSIjYzFjNWNhIi8+PHN0b3Agb2Zmc2V0PSI0My44JSIgc3RvcC1jb2xvcj0iI2JhYmZjNCIvPjxzdG9wIG9mZnNldD0iNTAuMCUiIHN0b3AtY29sb3I9IiNiNGI5YmUiLz48c3RvcCBvZmZzZXQ9IjU2LjMlIiBzdG9wLWNvbG9yPSIjYTlhZWIzIi8+PHN0b3Agb2Zmc2V0PSI2Mi41JSIgc3RvcC1jb2xvcj0iIzllYTNhOSIvPjxzdG9wIG9mZnNldD0iNjguOCUiIHN0b3AtY29sb3I9IiM5Mzk5OWYiLz48c3RvcCBvZmZzZXQ9Ijc1LjAlIiBzdG9wLWNvbG9yPSIjODg4ZTk1Ii8+PHN0b3Agb2Zmc2V0PSI4MS4zJSIgc3RvcC1jb2xvcj0iIzdlODQ4YSIvPjxzdG9wIG9mZnNldD0iODcuNSUiIHN0b3AtY29sb3I9IiM3MzdhODAiLz48c3RvcCBvZmZzZXQ9IjkzLjglIiBzdG9wLWNvbG9yPSIjNjk3MDc3Ii8+PHN0b3Agb2Zmc2V0PSIxMDAuMCUiIHN0b3AtY29sb3I9IiM1ZjY2NmQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iOC41IiB5MT0iMzYiIHgyPSI5MS41IiB5Mj0iMzYiPjxzdG9wIG9mZnNldD0iMC4wJSIgc3RvcC1jb2xvcj0iIzNmODZmZiIvPjxzdG9wIG9mZnNldD0iNi4zJSIgc3RvcC1jb2xvcj0iIzU1ODhmZiIvPjxzdG9wIG9mZnNldD0iMTIuNSUiIHN0b3AtY29sb3I9IiM2ODg5ZmYiLz48c3RvcCBvZmZzZXQ9IjE4LjglIiBzdG9wLWNvbG9yPSIjNzg4YWZmIi8+PHN0b3Agb2Zmc2V0PSIyNS4wJSIgc3RvcC1jb2xvcj0iIzg3OGJmZiIvPjxzdG9wIG9mZnNldD0iMzEuMyUiIHN0b3AtY29sb3I9IiM5NThjZmYiLz48c3RvcCBvZmZzZXQ9IjM3LjUlIiBzdG9wLWNvbG9yPSIjYTc4YWVmIi8+PHN0b3Agb2Zmc2V0PSI0My44JSIgc3RvcC1jb2xvcj0iI2I5ODZkNyIvPjxzdG9wIG9mZnNldD0iNTAuMCUiIHN0b3AtY29sb3I9IiNjYTgwYmYiLz48c3RvcCBvZmZzZXQ9IjU2LjMlIiBzdG9wLWNvbG9yPSIjZGE3OGE2Ii8+PHN0b3Agb2Zmc2V0PSI2Mi41JSIgc3RvcC1jb2xvcj0iI2VhNmU4YyIvPjxzdG9wIG9mZnNldD0iNjguOCUiIHN0b3AtY29sb3I9IiNmNTZkNzgiLz48c3RvcCBvZmZzZXQ9Ijc1LjAlIiBzdG9wLWNvbG9yPSIjZjc3ZjcyIi8+PHN0b3Agb2Zmc2V0PSI4MS4zJSIgc3RvcC1jb2xvcj0iI2Y4OTA2YiIvPjxzdG9wIG9mZnNldD0iODcuNSUiIHN0b3AtY29sb3I9IiNmOWExNjMiLz48c3RvcCBvZmZzZXQ9IjkzLjglIiBzdG9wLWNvbG9yPSIjZmFiMDU4Ii8+PHN0b3Agb2Zmc2V0PSIxMDAuMCUiIHN0b3AtY29sb3I9IiNmYWMwNGEiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZjIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMjguMDIxIiB5MT0iOTUuNDM1IiB4Mj0iMzkuOTc4IiB5Mj0iNC41NjQiPjxzdG9wIG9mZnNldD0iMC4wJSIgc3RvcC1jb2xvcj0iI2QwNmE0OSIvPjxzdG9wIG9mZnNldD0iNi4zJSIgc3RvcC1jb2xvcj0iI2QzNmY0ZiIvPjxzdG9wIG9mZnNldD0iMTIuNSUiIHN0b3AtY29sb3I9IiNkNjc0NTYiLz48c3RvcCBvZmZzZXQ9IjE4LjglIiBzdG9wLWNvbG9yPSIjZDk3YTVjIi8+PHN0b3Agb2Zmc2V0PSIyNS4wJSIgc3RvcC1jb2xvcj0iI2RjN2Y2MiIvPjxzdG9wIG9mZnNldD0iMzEuMyUiIHN0b3AtY29sb3I9IiNkZjg0NjgiLz48c3RvcCBvZmZzZXQ9IjM3LjUlIiBzdG9wLWNvbG9yPSIjZTI4OTZlIi8+PHN0b3Agb2Zmc2V0PSI0My44JSIgc3RvcC1jb2xvcj0iI2U1OGU3NCIvPjxzdG9wIG9mZnNldD0iNTAuMCUiIHN0b3AtY29sb3I9IiNlODkzN2EiLz48c3RvcCBvZmZzZXQ9IjU2LjMlIiBzdG9wLWNvbG9yPSIjZWI5ZDg1Ii8+PHN0b3Agb2Zmc2V0PSI2Mi41JSIgc3RvcC1jb2xvcj0iI2VlYTY4ZiIvPjxzdG9wIG9mZnNldD0iNjguOCUiIHN0b3AtY29sb3I9IiNmMWIwOWEiLz48c3RvcCBvZmZzZXQ9Ijc1LjAlIiBzdG9wLWNvbG9yPSIjZjRiYWE0Ii8+PHN0b3Agb2Zmc2V0PSI4MS4zJSIgc3RvcC1jb2xvcj0iI2Y2YzNhZiIvPjxzdG9wIG9mZnNldD0iODcuNSUiIHN0b3AtY29sb3I9IiNmOWNkYjkiLz48c3RvcCBvZmZzZXQ9IjkzLjglIiBzdG9wLWNvbG9yPSIjZmJkNmM0Ii8+PHN0b3Agb2Zmc2V0PSIxMDAuMCUiIHN0b3AtY29sb3I9IiNmZGUwY2YiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZjMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNzEuOTc4IiB5MT0iOTUuNDM1IiB4Mj0iNjAuMDIxIiB5Mj0iNC41NjQiPjxzdG9wIG9mZnNldD0iMC4wJSIgc3RvcC1jb2xvcj0iIzRhNWNmZiIvPjxzdG9wIG9mZnNldD0iNi4zJSIgc3RvcC1jb2xvcj0iIzUwNjJmZiIvPjxzdG9wIG9mZnNldD0iMTIuNSUiIHN0b3AtY29sb3I9IiM1NzY4ZmYiLz48c3RvcCBvZmZzZXQ9IjE4LjglIiBzdG9wLWNvbG9yPSIjNWQ2ZGZmIi8+PHN0b3Agb2Zmc2V0PSIyNS4wJSIgc3RvcC1jb2xvcj0iIzY0NzJmZiIvPjxzdG9wIG9mZnNldD0iMzEuMyUiIHN0b3AtY29sb3I9IiM2Yjc3ZmYiLz48c3RvcCBvZmZzZXQ9IjM3LjUlIiBzdG9wLWNvbG9yPSIjNzE3Y2ZmIi8+PHN0b3Agb2Zmc2V0PSI0My44JSIgc3RvcC1jb2xvcj0iIzc4ODFmZiIvPjxzdG9wIG9mZnNldD0iNTAuMCUiIHN0b3AtY29sb3I9IiM3Zjg2ZmYiLz48c3RvcCBvZmZzZXQ9IjU2LjMlIiBzdG9wLWNvbG9yPSIjODk5MGZmIi8+PHN0b3Agb2Zmc2V0PSI2Mi41JSIgc3RvcC1jb2xvcj0iIzkzOTlmZiIvPjxzdG9wIG9mZnNldD0iNjguOCUiIHN0b3AtY29sb3I9IiM5ZGEyZmYiLz48c3RvcCBvZmZzZXQ9Ijc1LjAlIiBzdG9wLWNvbG9yPSIjYThhYmZmIi8+PHN0b3Agb2Zmc2V0PSI4MS4zJSIgc3RvcC1jb2xvcj0iI2IzYjRmZiIvPjxzdG9wIG9mZnNldD0iODcuNSUiIHN0b3AtY29sb3I9IiNiZWJkZmYiLz48c3RvcCBvZmZzZXQ9IjkzLjglIiBzdG9wLWNvbG9yPSIjYzljNWZmIi8+PHN0b3Agb2Zmc2V0PSIxMDAuMCUiIHN0b3AtY29sb3I9IiNkNGNlZmYiLz48L2xpbmVhckdyYWRpZW50PjxjbGlwUGF0aCBpZD0iZjQiPjxwb2x5Z29uIHBvaW50cz0iMTYsNDYuNSA4NCw0Ni41IDg0LDI1LjUgMTYsMjUuNSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJmNSI+PHBvbHlnb24gcG9pbnRzPSIxNiw0OC41IDg0LDQ4LjUgODQsMjMuNSAxNiwyMy41Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImY2Ij48cG9seWdvbiBwb2ludHM9IjgxLjQxMCw4Ni42MzAgNzEuNDEwLDEwLjYzMCA1MC41ODksMTMuMzY5IDYwLjU4OSw4OS4zNjkiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZjciPjxwb2x5Z29uIHBvaW50cz0iODMuMzkzLDg2LjM2OSA3My4zOTMsMTAuMzY5IDQ4LjYwNiwxMy42MzAgNTguNjA2LDg5LjYzMCIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJmOCI+PHBvbHlnb24gcG9pbnRzPSIxMiw3NC41IDg4LDc0LjUgODgsNTMuNSAxMiw1My41Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImY5Ij48cG9seWdvbiBwb2ludHM9IjEyLDc2LjUgODgsNzYuNSA4OCw1MS41IDEyLDUxLjUiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZjEwIj48cG9seWdvbiBwb2ludHM9IjM5LjQxMCw4OS4zNjkgNDkuNDEwLDEzLjM2OSAyOC41ODksMTAuNjMwIDE4LjU4OSw4Ni42MzAiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZjExIj48cG9seWdvbiBwb2ludHM9IjQxLjM5Myw4OS42MzAgNTEuMzkzLDEzLjYzMCAyNi42MDYsMTAuMzY5IDE2LjYwNiw4Ni4zNjkiLz48L2NsaXBQYXRoPjwvZGVmcz48bGluZSB4MT0iMTIiIHkxPSI2NCIgeDI9Ijg4IiB5Mj0iNjQiIHN0cm9rZT0idXJsKCNmMCkiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxsaW5lIHgxPSIxNiIgeTE9IjM2IiB4Mj0iODQiIHkyPSIzNiIgc3Ryb2tlPSJ1cmwoI2YxKSIgc3Ryb2tlLXdpZHRoPSIxNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PGxpbmUgeDE9IjI5IiB5MT0iODgiIHgyPSIzOSIgeTI9IjEyIiBzdHJva2U9InVybCgjZjIpIiBzdHJva2Utd2lkdGg9IjE1IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bGluZSB4MT0iNzEiIHkxPSI4OCIgeDI9IjYxIiB5Mj0iMTIiIHN0cm9rZT0idXJsKCNmMykiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxnIGNsaXAtcGF0aD0idXJsKCNmNCkiPjxsaW5lIHgxPSIyOSIgeTE9Ijg4IiB4Mj0iMzkiIHkyPSIxMiIgc3Ryb2tlPSIjMTUxOTFhIiBzdHJva2Utd2lkdGg9IjIxIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L2c+PGcgY2xpcC1wYXRoPSJ1cmwoI2Y1KSI+PGxpbmUgeDE9IjI5IiB5MT0iODgiIHgyPSIzOSIgeTI9IjEyIiBzdHJva2U9InVybCgjZjIpIiBzdHJva2Utd2lkdGg9IjE1IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L2c+PGcgY2xpcC1wYXRoPSJ1cmwoI2Y2KSI+PGxpbmUgeDE9IjE2IiB5MT0iMzYiIHgyPSI4NCIgeTI9IjM2IiBzdHJva2U9IiMxNTE5MWEiIHN0cm9rZS13aWR0aD0iMjEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvZz48ZyBjbGlwLXBhdGg9InVybCgjZjcpIj48bGluZSB4MT0iMTYiIHkxPSIzNiIgeDI9Ijg0IiB5Mj0iMzYiIHN0cm9rZT0idXJsKCNmMSkiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvZz48ZyBjbGlwLXBhdGg9InVybCgjZjgpIj48bGluZSB4MT0iNzEiIHkxPSI4OCIgeDI9IjYxIiB5Mj0iMTIiIHN0cm9rZT0iIzE1MTkxYSIgc3Ryb2tlLXdpZHRoPSIyMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCNmOSkiPjxsaW5lIHgxPSI3MSIgeTE9Ijg4IiB4Mj0iNjEiIHkyPSIxMiIgc3Ryb2tlPSJ1cmwoI2YzKSIgc3Ryb2tlLXdpZHRoPSIxNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCNmMTApIj48bGluZSB4MT0iMTIiIHkxPSI2NCIgeDI9Ijg4IiB5Mj0iNjQiIHN0cm9rZT0iIzE1MTkxYSIgc3Ryb2tlLXdpZHRoPSIyMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCNmMTEpIj48bGluZSB4MT0iMTIiIHkxPSI2NCIgeDI9Ijg4IiB5Mj0iNjQiIHN0cm9rZT0idXJsKCNmMCkiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvZz48L2c+PC9zdmc+">
|