从你自己的代码运行实验日志
计量的通道可以通过 HTTP 调用,返回一个 JSON 对象。分工是:页面按 skill 的规则生成实验 ID 和样品批次,按示例日志的字段写好 YAML frontmatter,从笔记里提取数值和温度程序,把模糊、缺失、矛盾和缺单位的值列成待确认行,算好派生数,再把这些作为事实交给运行。运行只写日志正文和问题;页面再用自己的事实核对它(labreply.js)。frontmatter 始终是页面的。
基础 URL 和响应的形状
所有接口都在 https://api.skillsafe.ai/v1/app-api 下面,每个响应都是同一个形状:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
token 是为这个 app 生成的(访客调用带 {"slug":"lab-log"}),之后不需要 slug 头。用 Authorization: Bearer YOUR_TOKEN 发送。运行的请求体就是输入对象,裸着发。
| 状态 | 代码 | 含义 |
|---|---|---|
| 400 | validation_error | 请求体不是 JSON 对象,或者缺了必填的键(task、entry、notes、facts)。页面总是把四个都发过去。 |
| 401 | unauthorized | 没有 token,或者 token 过期了。生成访客 token 或重新登录。 |
| 402 | insufficient_credits | 余额低于 min_credits。先用 /estimate 估价。 |
| 403 | forbidden | 访客 token 想运行:这个通道是计量的,需要个人登录。 |
| 404 | not_found | 未知的任务 id。 |
| 429 | rate_limited | 等一会儿再试。 |
| 5xx | server_error | 临时故障。用同一个 Idempotency-Key 重试,重试不会重复收费。 |
先看 task 字段
一个通道,由 task 选择:
log- 日志正文的六节(实验目的、实验步骤、观察、结果、异常、下一步),先按顺序逐条问页面的待确认行,之后最多 3 个问题,最多 6 个标签,以及注意事项。
缺了或是未知的 task 按 log 回答。facts 是页面的结论、待确认行(Q1、Q2 …)、每个字段的值和来源、派生数和检查;以 "must:" 开头的一行是回复必须遵守的规则。
输入
| 字段 | 必填 | 内容 |
|---|---|---|
task | 是 | 固定为 log。缺失或是别的值时,按 log 做,并写 "task": "log"。 |
entry | 是 | 页面整理好的这条记录:标题、体系和体系代码、实验类型、字段模板、设备、日期、实验 ID、批次、是否异常、日志路径、原始材料归档和附件,以及页面写好的 frontmatter。 |
notes | 是 | 用户的原始笔记(文字或语音转录),原样照录。 |
facts | 是 | 页面的结论;待确认行(Q1、Q2 …,每行有字段名、来源和页面要问的问题);每个字段的值和来源;派生数和它的公式;检查结果。以 "must:" 开头的一行是你的回复必须遵守的规则。 |
retry_note | 否 | 可选;出现时说明你上一次的回复没法用,照它说的改。 |
模型遵循的规则
来自源 skill @yuan1z0825/nature-experiment-log - 运行唯一遵循的标准:
- 对缺失或模糊字段向用户确认,不猜测实验条件或结果;模糊信息(温度记不清、样品编号不明)主动询问,不猜测写入。
- 不编造温度、时长、配方、设备型号或结果;未确认的信息保留为中文的确认项。
- 正文按示例日志的结构写:实验目的、实验步骤(编号)、观察、结果、异常、下一步;最后的「原始材料」一节由页面写,引用归档文件夹和每个附件。
- 同一批样品跨多个实验时,批次 ID 保持一致,便于追踪。
- 发现异常(设备故障、操作失误、结果异常、安全事件,或偏离方案)时,日志里写清异常,并追加一条异常记录。
- 产出的是可以直接保存的 Markdown:带 YAML frontmatter 的日志、附件索引、缺失信息清单和下一步实验建议。
其余规则
- 只用输入里的内容。frontmatter 是页面的:不改写,不在正文里写成 key: value,也不写 --- 分隔线。
- 不猜:待确认的字段在正文里不给值,直接写「待确认」(例如「测试温度待确认」);不要把笔记里的估计值、或者一个范围里的某一个数,当成结果写进正文。
- 正文里出现的每个数字连同它的单位,都必须在笔记、facts 的字段值或派生数里出现过;不自己换算、求和或算百分比,页面算好的派生数可以照写。不编造条件、试剂、仪器、样品编号、批次或结果。
- 不写任何人的名字,不写操作人,也不问是谁做的实验。
- 不写链接、网址;任何文字里都不用 ` 这个字符(反引号)。不要把输入称作字段、数据或 entry、notes、facts。
- 用简体中文写;化学式、仪器缩写和编号照原样写(如 Ar、SEM-EDS、316L)。上面规则里的词可以自由使用;不要整句照抄本提示里的句子。
- 只回复一个 JSON 对象,前后不加任何文字,不加代码围栏。
回复
log
把笔记整理成日志正文。
summary- 一句话:这次做了什么,以及页面的结论(可以写日志,或者要先回答待确认问题)。sections- 一个对象,六个键照写:实验目的(一段话:这次要验证或测定什么,只写笔记里有的)、实验步骤(数组:按做的顺序,一条一步,不要自己加编号,页面会编号)、观察(数组:笔记里记下的现象,一条一个)、结果(数组:笔记里的测量结果和页面的派生数;待确认的写「待确认」)、异常(一段话:没有异常时只写「无。」;有异常时客观写出笔记里的异常现象)、下一步(数组:笔记里提到的后续安排,也可以按结果提建议,但不带新的数字)。questions- 数组:先按 facts 里 Q1、Q2 … 的顺序逐条问,每条写出该行的字段名(如 temperature)并以问号结尾;之后最多再问 3 个笔记没交代清楚的问题。每一条都是问题,不是答案。没有待确认行时,只问你自己的(最多 3 个,也可以没有)。tags- 最多 6 个标签,用笔记或表单里的原词(体系、实验类型、材料、方法)。caveats- 数组:页面确认不了、需要用户自己核对的地方,一句一条(可以为空)。
{
"task": "log",
"summary": "一句话",
"sections": { "实验目的": "一段话", "实验步骤": ["第一步", "第二步"], "观察": ["一条现象"], "结果": ["一条结果;待确认的写待确认"], "异常": "无。", "下一步": ["一条后续安排"] },
"questions": ["Q1 的问题,写出字段名?", "一个补充问题?"],
"tags": ["体系名称", "实验类型"],
"caveats": ["一句话"]
}
页面用自己的事实核对它(labreply.js 里的 check();有任何提醒就算 flagged):回答的是发过去的 task;没有链接、没有反引号、不提输入的字段名、不写人名或操作人;不在正文里改写 frontmatter;正文是中文;五个字段都在,summary 是一句话;六节都在,实验目的和实验步骤不为空,步骤的编号(如果有)从 1 起按顺序;有异常时「异常」写清现象,没有时写「无。」;正文里每个数字连同单位都能在笔记、已知字段、页面的字段或派生数里找到(300°C、300 °C、300℃ 视为同一个),带数字的编号也要找得到;待确认的字段在正文里不给值,正文里至少有一处「待确认」;questions 的前几条按顺序对应每一行待确认并写出字段名,之后最多 3 条,每条以问号结尾;tags 最多 6 个,每个都是笔记或表单里的原词。
完整示例
页面三个免费示例的运行(数据都是虚构的):腐蚀浸泡(可以写,腐蚀速率算出来了)、电化学 CV(有 3 行待确认)、热稳定性(可以写,带异常记录)。每个输入都是页面实际发送的;每个回复都是示例重放的保存回复,没有改动 - 但长字段只在这里的显示中被截短,每处截短都列在示例下面。最后一行是页面对完整回复的检查。
log - 腐蚀浸泡:316L 在 Q2 氯盐中 300 h
页面这样读它:可以写日志(模板 腐蚀浸泡),实验 ID CL-M-260603-001,批次 CL-Q2-B001;待确认:没有。
输入(facts 有 1414 个字符,显示前 1200 个):
{
"task": "log",
"entry": "标题:Q2腐蚀验证_316L\n体系:氯盐(体系代码 CL);实验类型:腐蚀验证;字段模板:腐蚀浸泡(references/example-log.md)\n设备:M 马弗炉(热处理、浸泡腐蚀);日期:2026-06-03\n实验 ID:CL-M-260603-001;salt_batch:CL-Q2-B001\n异常:否\n日志文件:我的实验记录/实验日志/氯盐/腐蚀验证/CL-M-260603-001.md\n原始材料归档:raw/experiments/2026.06.03_Q2腐蚀验证_316L_CL-M-260603-001/;附件:图片/IMG_6031.jpg - 腐蚀前样品表面;图片/IMG_6032.jpg - 腐蚀后样品表面;图片/IMG_6033.jpg - 盐冷却后坩埚内部\n\n页面写好的 frontmatter(页面的,不由你改写):\n---\nexp_id: CL-M-260603-001\ndate: 2026-06-03\nsalt_system: 氯盐\nsalt_batch: CL-Q2-B001\nsalt_composition: Q2 (*** mol%)\nexp_type: 腐蚀验证\nfurnace: 马弗炉_1号\ncrucible: AL-002\ntotal_salt_mass_g: 50.0\ntemperature_profile: \"RT→300°C(2h,干燥)→500°C(300h)→炉冷\"\natmosphere: Ar\nmaterial: 316L\nmaterial_category: 奥氏体不锈钢\nsample_id: 316L-2026-004\nsample_dimensions_mm: 20×10×2\nsample_surface: SiC 1200目\npre_mass_g: 3.4612\npost_mass_g: 3.4571\nmass_loss_g: 0.0041\ncorrosion_rate_um_year: 28.85\nanomaly: false\nanomaly_ref:\ntags: []\n---",
"notes": "配制 Q2 盐 50.0 g,手套箱中混合均匀,装入坩埚 AL-002。\n316L 样品 316L-2026-004,尺寸 20×10×2 mm,SiC 1200目打磨,丙酮超声清洗,干燥后称重 3.4612 g。\n马弗炉_1号,Ar 气氛,温度程序 RT→300°C(2h,干燥)→500°C(300h)→炉冷。\n炉冷至室温后取出样品,去离子水超声清洗,干燥后称重 3.4571 g。\n观察:盐冷却后呈浅黄色透明玻璃态,无明显分层;样品表面失去金属光泽,呈均匀暗灰色;坩埚内壁无可见腐蚀痕迹;未观察到气体释放或飞溅。\n之后要做 SEM-EDS 截面分析和 XRD。",
"facts": "页面结论:没有待确认的字段:可以写日志。待确认 0 行。\n待确认:没有。\n字段(页面确认的值;待确认的留空):\n- exp_id = CL-M-260603-001 [页面]\n- date = 2026-06-03 [页面]\n- salt_system = 氯盐 [页面]\n- salt_batch = CL-Q2-B001 [页面]\n- salt_composition = Q2 (*** mol%) [已知字段]\n- exp_type = 腐蚀验证 [页面]\n- furnace = 马弗炉_1号 [已知字段]\n- crucible = AL-002 [已知字段]\n- total_salt_mass_g = 50.0 [已知字段]\n- temperature_profile = RT→300°C(2h,干燥)→500°C(300h)→炉冷 [笔记]\n- atmosphere = Ar [笔记]\n- material = 316L [已知字段]\n- material_category = 奥氏体不锈钢 [已知字段]\n- sample_id = 316L-2026-004 [已知字段]\n- sample_dimensions_mm = 20×10×2 [笔记]\n- sample_surface = SiC 1200目 [已知字段]\n- pre_mass_g = 3.4612 [已知字段]\n- post_mass_g = 3.4571 [已知字段]\n- mass_loss_g = 0.0041 [派生]\n- corrosion_rate_um_year = 28.85 [派生]\n- anomaly = false [页面]\n- anomaly_ref = (空) [留空]\n- tags = (空) [由你的 tags 填]\n页面派生数(正文可以照写):\n- 总保温时长:2 h + 300 h = 302 h\n- 失重 mass_loss_g:pre_mass_g − post_mass_g = 3.4612 − 3.4571 = 0.0041 g\n- 失重率:0.0041 / 3.4612 × 100 = 0.12%\n- 样品表面积(矩形试样):2(ab+ac+bc) = 2(20×10 + 20×2 + 10×2) = 520 mm² = 5.2 cm²\n- 腐蚀速率 corrosion_rate_um_year:8.76×10^7 × Δm / (A × t × ρ) = 8.76×10^7 × 0.0041 / (5.2 × 300 × 7.98) = 28.85 µm/年(ASTM G1 公式,非 skill 内容)\n检查:id ok;batch ok;fields ok;ambiguity ok;profile ok;units ok;attachmen\n\n[……这里的显示截掉了 214 个字符……]"
}
回复:
{
"task": "log",
"summary": "验证候选配方 Q2 在 500°C、Ar 气氛下对 316L 的腐蚀行为,失重 0.0041 g(0.12%),页面判定可以写日志。",
"sections": {
"实验目的": "验证候选配方 Q2 在 500°C 下对 316L 奥氏体不锈钢的腐蚀行为。",
"实验步骤": [
"配制 Q2 盐 50.0 g,在手套箱中混合均匀,装入坩埚 AL-002",
"316L 样品 316L-2026-004(20×10×2 mm)用 SiC 1200目打磨,丙酮超声清洗,干燥后称重 3.4612 g",
"在马弗炉_1号中、Ar 气氛下运行温度程序 RT→300°C(2h,干燥)→500°C(300h)→炉冷",
"炉冷至室温后取出样品,去离子水超声清洗",
"干燥后称重 3.4571 g,计算失重和腐蚀速率"
],
"观察": [
"盐冷却后呈浅黄色透明玻璃态,无明显分层",
"样品表面失去金属光泽,呈均匀暗灰色",
"坩埚内壁无可见腐蚀痕迹",
"未观察到气体释放或飞溅"
],
"结果": [
"失重 0.0041 g(3.4612 g − 3.4571 g),失重率 0.12%",
"样品表面积 5.2 cm²(20×10×2 mm 的矩形试样)",
"腐蚀速率 28.85 µm/年,按 ASTM G1 公式用确认的密度 7.98 g/cm³ 和暴露时长 300 h 算出",
"腐蚀产物组成要等 SEM-EDS 和 XRD 确认"
],
"异常": "无。",
"下一步": [
"做 SEM-EDS 截面分析,看腐蚀层结构",
"做 XRD,确认腐蚀产物的相组成"
]
},
"questions": [
"密度 7.98 g/cm³ 是这批 316L 的实测值还是手册值?"
],
"tags": [
"氯盐",
"腐蚀验证",
"316L",
"Q2"
],
"caveats": [
"腐蚀速率用的是 ASTM G1 公式,不是源 skill 的内容,密度和暴露时长以你的确认为准",
"笔记没有记录是否有点蚀,需要显微观察确认"
]
}
第 5 步为它打印的要点:验证候选配方 Q2 在 500°C、Ar 气氛下对 316L 的腐蚀行为,失重 0.0041 g(0.12%),页面判定可以写日志。 | 1. 密度 7.98 g/cm³ 是这批 316L 的实测值还是手册值?
页面的检查:kept - 没有要提醒的
log - 电化学 CV:语音转录,有待确认
页面这样读它:先回答待确认问题(模板 电化学),实验 ID OX-E-260616-001,批次 待确认;待确认:Q1 salt_batch、Q2 temperature、Q3 potential_window_V。
输入(facts 有 1451 个字符,显示前 1200 个):
{
"task": "log",
"entry": "标题:A2电化学窗口测试\n体系:氧化物体系(体系代码 OX);实验类型:电化学;字段模板:电化学(references/example-electrochemical.md)\n设备:E 电化学工作站(CV/SWV/EIS);日期:2026-06-16\n实验 ID:OX-E-260616-001;salt_batch:待确认\n异常:否\n日志文件:我的实验记录/实验日志/氧化物体系/电化学/OX-E-260616-001.md\n原始材料归档:raw/experiments/2026.06.16_A2电化学窗口测试_OX-E-260616-001/;附件:图片/IMG_6161.jpg - 三电极测试台布置;语音/录音_0616.m4a - 测试时的口述\n\n页面写好的 frontmatter(页面的,不由你改写):\n---\nexp_id: OX-E-260616-001\ndate: 2026-06-16\nsalt_system: 氧化物体系\nsalt_batch:\nsalt_composition: \"*** (*** wt%)\"\nexp_type: 电化学\ntest_method: CV\nworking_electrode: Pt丝\ncounter_electrode: Pt网\nreference_electrode: Ag/Ag+ 准参比\ntemperature:\natmosphere: Ar\npotential_window_V:\nscan_rate_mV_s: 50\nevaluation_target: 测定 A2 的电化学窗口,检查杂质峰\nanomaly: false\nanomaly_ref:\ntags: []\n# 待确认:\n# - salt_batch: 笔记写「用的是哪一批记不清了」:配盐批次(salt_batch)是哪一批(OX-A2-B 几)?\n# - temperature: 笔记写「温度大概 450 左右」:温度(temperature)的确切值是多少,单位是 °C 还是 K?\n# - potential_window_V: 笔记写「电位窗口我记得是从 -1.5 V 扫到 0.5-0.8 V」,电位窗口(potential_window_V)需要一个确定的值:是 0.5 V 还是 0.8 V,或别的值?\n---",
"notes": "【语音转录】今天做 A2 盐的 CV 窗口测试。\nA2 盐 30 g 是在手套箱里配的,用的是哪一批记不清了,可能是第二批?\n先在马弗炉里 300°C 干燥 2h,去除吸附水。\n转到电化学测试台,Ar 气氛,温度大概 450 左右。\n三电极:Pt 丝工作电极,Pt 网对电极,Ag/Ag+ 准参比。\n电位窗口我记得是从 -1.5 V 扫到 0.5-0.8 V,扫速 50 mV/s,记了 3 圈,取第 3 圈。\n盐熔化后是透明液体,没看到悬浮物;曲线在 -0.8 V 附近有个小峰;电极表面没看到腐蚀。",
"facts": "页面结论:有待确认的字段:日志可以起草,但要先问清楚。待确认 3 行。\n待确认(按顺序;questions 的前 3 条逐条对应,每条写出该行的字段名(不对应字段的写它的标签和值)并以问号结尾):\n- Q1 | salt_batch | 配盐批次(salt_batch) | 来源:笔记:用的是哪一批记不清了 | 页面的问题:笔记写「用的是哪一批记不清了」:配盐批次(salt_batch)是哪一批(OX-A2-B 几)?\n- Q2 | temperature | 温度(temperature) | 来源:笔记:温度大概 450 左右 | 页面的问题:笔记写「温度大概 450 左右」:温度(temperature)的确切值是多少,单位是 °C 还是 K?\n- Q3 | potential_window_V | 电位窗口(potential_window_V) | 来源:笔记:电位窗口我记得是从 -1.5 V 扫到 0.5-0.8 V | 页面的问题:笔记写「电位窗口我记得是从 -1.5 V 扫到 0.5-0.8 V」,电位窗口(potential_window_V)需要一个确定的值:是 0.5 V 还是 0.8 V,或别的值?\n字段(页面确认的值;待确认的留空):\n- exp_id = OX-E-260616-001 [页面]\n- date = 2026-06-16 [页面]\n- salt_system = 氧化物体系 [页面]\n- salt_batch = (空) [待确认 Q1]\n- salt_composition = *** (*** wt%) [已知字段]\n- exp_type = 电化学 [页面]\n- test_method = CV [已知字段]\n- working_electrode = Pt丝 [已知字段]\n- counter_electrode = Pt网 [已知字段]\n- reference_electrode = Ag/Ag+ 准参比 [已知字段]\n- temperature = (空) [待确认 Q2]\n- atmosphere = Ar [笔记]\n- potential_window_V = (空) [待确认 Q3]\n- scan_rate_mV_s = 50 [已知字段]\n- evaluation_target = 测定 A2 的电化学窗口,检查杂质峰 [已知字段]\n- anomaly = false [页面]\n- anomaly_ref = (空) [留空]\n- tags = (空) [由你的 tags 填]\n页面派生数:没有。\n检查:id ok;batch warn;fields warn;ambiguity warn;units warn;attachments ok;anomaly ok;yaml ok。\nmust: 只写正\n\n[……这里的显示截掉了 251 个字符……]"
}
回复:
{
"task": "log",
"summary": "A2 盐在 Ar 气氛下做了 CV 窗口测试,但配盐批次、测试温度和电位窗口上限待确认,要先回答问题再定稿。",
"sections": {
"实验目的": "测定 A2 的电化学窗口,检查杂质峰。",
"实验步骤": [
"在手套箱中配制 A2 盐 30 g(批次待确认)",
"在马弗炉中 300°C 干燥 2h,去除吸附水",
"转到电化学测试台,在 Ar 气氛下升温(测试温度待确认)",
"三电极体系:Pt 丝工作电极、Pt 网对电极、Ag/Ag+ 准参比",
"CV 扫描从 -1.5 V 起,正向上限待确认,扫速 50 mV/s",
"记录 3 圈,取第 3 圈"
],
"观察": [
"盐熔化后是透明液体,没看到悬浮物",
"曲线在 -0.8 V 附近有一个小峰",
"电极表面没看到腐蚀"
],
"结果": [
"电化学窗口的下限为 -1.5 V,上限待确认,窗口宽度要等上限确认后才能给出",
"-0.8 V 附近小峰的来源待确认"
],
"异常": "无。",
"下一步": [
"确认配盐批次、测试温度和电位窗口上限后补全日志",
"查明 -0.8 V 附近小峰的来源"
]
},
"questions": [
"配盐批次(salt_batch)用的是 A2 的哪一批,OX-A2-B 几?",
"测试温度(temperature)的确切值是多少,单位是 °C 还是 K?",
"电位窗口(potential_window_V)的正向上限是 0.5 V 还是 0.8 V,或者别的值?",
"曲线上除了 -0.8 V 附近,还有别的峰吗?"
],
"tags": [
"氧化物体系",
"电化学",
"CV",
"A2"
],
"caveats": [
"配盐批次、测试温度和电位窗口上限确认之前,这份日志只是草稿",
"-0.8 V 附近的小峰是否来自杂质,笔记里没有判断依据"
]
}
第 5 步为它打印的要点:A2 盐在 Ar 气氛下做了 CV 窗口测试,但配盐批次、测试温度和电位窗口上限待确认,要先回答问题再定稿。 | 1. 配盐批次(salt_batch)用的是 A2 的哪一批,OX-A2-B 几? | 2. 测试温度(temperature)的确切值是多少,单位是 °C 还是 K? | 3. 电位窗口(potential_window_V)的正向上限是 0.5 V 还是 0.8 V,或者别的值? | 4. 曲线上除了 -0.8 V 附近,还有别的峰吗?
页面的检查:kept - 没有要提醒的
log - 热稳定性:坩埚开裂的异常
页面这样读它:可以写日志(模板 热稳定性),实验 ID HY-T-260602-003,批次 HY-B1-B001;待确认:没有;有异常,追加一条异常记录。
输入:
{
"task": "log",
"entry": "标题:B1热稳定性复测\n体系:混合盐体系(体系代码 HY);实验类型:热稳定性;字段模板:热稳定性(references/example-thermal-stability.md)\n设备:T 管式炉(气氛控制、脱水、热稳定性);日期:2026-06-02\n实验 ID:HY-T-260602-003;salt_batch:HY-B1-B001(复用已有批次)\n异常:是(设备故障 / 中 / 需复查)\n日志文件:我的实验记录/实验日志/混合盐体系/热稳定性/HY-T-260602-003.md\n原始材料归档:raw/experiments/2026.06.02_B1热稳定性复测_HY-T-260602-003/;附件:图片/IMG_6021.jpg - 坩埚侧壁裂纹;图片/IMG_6022.jpg - 垫片上渗出的盐\n\n页面写好的 frontmatter(页面的,不由你改写):\n---\nexp_id: HY-T-260602-003\ndate: 2026-06-02\nsalt_system: 混合盐体系\nsalt_batch: HY-B1-B001\nsalt_composition: \"*** (*** wt%)\"\nexp_type: 热稳定性\nfurnace: 管式炉_1号\ncrucible: AL-006\ntotal_salt_mass_g: 40.0\ntemperature_profile: \"RT→200°C(4h,脱水)→500°C(8h)→700°C(2h)→炉冷\"\natmosphere: Ar (100 mL/min)\ncondensate: 收集到 2.1 g 无色液体\nabsorption_solution: 0.1M NaOH 50mL\nanomaly: true\nanomaly_ref: 实验日志/异常记录.md\ntags: []\n---",
"notes": "B1 盐 40.0 g,沿用 HY-B1-B001 这一批,手套箱中转入坩埚 AL-006。\n管式炉_1号,Ar (100 mL/min),温度程序 RT→200°C(4h,脱水)→500°C(8h)→700°C(2h)→炉冷。\n出气口接冷凝管和 0.1M NaOH 50mL 吸收瓶。\n500°C 段冷凝管收集到 2.1 g 无色液体。\n700°C 段结束、炉冷后发现坩埚左右两侧各有一道裂纹,部分盐渗到坩埚外的垫片上。\n炉冷后称重,坩埚内剩余盐 35.2 g。",
"facts": "页面结论:没有待确认的字段:可以写日志。待确认 0 行。\n待确认:没有。\n字段(页面确认的值;待确认的留空):\n- exp_id = HY-T-260602-003 [页面]\n- date = 2026-06-02 [页面]\n- salt_system = 混合盐体系 [页面]\n- salt_batch = HY-B1-B001 [页面]\n- salt_composition = *** (*** wt%) [已知字段]\n- exp_type = 热稳定性 [页面]\n- furnace = 管式炉_1号 [已知字段]\n- crucible = AL-006 [已知字段]\n- total_salt_mass_g = 40.0 [已知字段]\n- temperature_profile = RT→200°C(4h,脱水)→500°C(8h)→700°C(2h)→炉冷 [笔记]\n- atmosphere = Ar (100 mL/min) [已知字段]\n- condensate = 收集到 2.1 g 无色液体 [已知字段]\n- absorption_solution = 0.1M NaOH 50mL [已知字段]\n- anomaly = true [页面]\n- anomaly_ref = 实验日志/异常记录.md [页面]\n- tags = (空) [由你的 tags 填]\n页面派生数(正文可以照写):\n- 总保温时长:4 h + 8 h + 2 h = 14 h\n检查:id ok;batch ok;fields ok;ambiguity ok;profile ok;units ok;attachments ok;anomaly ok;yaml ok。\nmust: 只写正文(summary、sections、questions、tags、caveats),frontmatter 是页面的;正文里的每个数字连同单位都要在笔记、已知字段、上面的字段或派生数里出现过;questions 之后最多再问 3 个;tags 最多 6 个,用笔记里的原词;异常:写清笔记里的异常现象;不写人名、不写链接、不用反引号;只用中文。"
}
回复:
{
"task": "log",
"summary": "B1 盐沿用 HY-B1-B001 这一批在管式炉_1号中做热稳定性复测,炉冷后发现坩埚开裂,页面判定可以写日志并追加异常记录。",
"sections": {
"实验目的": "复测候选配方 B1 在 Ar 保护气氛下的热稳定性。",
"实验步骤": [
"沿用 HY-B1-B001 这一批 B1 盐 40.0 g,在手套箱中转入坩埚 AL-006",
"在管式炉_1号中通 Ar (100 mL/min),运行温度程序 RT→200°C(4h,脱水)→500°C(8h)→700°C(2h)→炉冷",
"出气口接冷凝管和 0.1M NaOH 50mL 吸收瓶",
"炉冷后称重坩埚内剩余的盐"
],
"观察": [
"500°C 段冷凝管收集到 2.1 g 无色液体",
"炉冷后发现坩埚左右两侧各有一道裂纹,部分盐渗到坩埚外的垫片上"
],
"结果": [
"炉冷后坩埚内剩余盐 35.2 g",
"冷凝管收集到 2.1 g 无色液体",
"坩埚开裂、盐渗出,剩余盐的质量不能直接用来判断热稳定性"
],
"异常": "700°C 段结束、炉冷后发现坩埚 AL-006 左右两侧各有一道裂纹,部分盐渗到坩埚外的垫片上。",
"下一步": [
"检查坩埚 AL-006 的裂纹,并在设备与试剂追踪里更新它的状态",
"换一个坩埚复测 B1 的热稳定性",
"收集并称量垫片上渗出的盐"
]
},
"questions": [
"冷凝管里的无色液体有没有做成分分析?",
"坩埚 AL-006 开裂前用过几次?"
],
"tags": [
"热稳定性",
"管式炉",
"混合盐体系",
"B1"
],
"caveats": [
"笔记只记到炉冷后发现裂纹,坩埚是在哪一段开裂的无法确定",
"异常记录里的影响和处理两节要你自己补写"
]
}
第 5 步为它打印的要点:B1 盐沿用 HY-B1-B001 这一批在管式炉_1号中做热稳定性复测,炉冷后发现坩埚开裂,页面判定可以写日志并追加异常记录。 | 1. 冷凝管里的无色液体有没有做成分分析? | 2. 坩埚 AL-006 开裂前用过几次?
页面的检查:kept - 没有要提醒的
自己构造输入
在 node 里载入引擎(global.window = global; require("./labkit.js"); require("./labreply.js")),然后:
const K = LabLogKit, R = LabLogReply;
const f = K.form(Object.assign(K.blank(), { title: "B1热稳定性", system_name: "混合盐体系", system_code: "HY", exp_type: "热稳定性",
template: "thermal", device: "T", date: "2026-06-01", notes: "温度大概 500 左右,RT→200°C(4h,脱水)→500°C(8h)→炉冷" }));
// f.values (每个字段的值和来源), f.pending (待确认行和问题), f.yaml, f.derived, f.paths, f.checks, f.verdict
const b = K.buildInput(f, "log");
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "log");
// R.check(reply, b) // {status: kept or flagged, notes}
下面几步用的 token 从 /tokens.html 拿。
第 1 步 — 一个最小的客户端
一个函数:发送 JSON,拆开响应。从 /tokens.html 贴一个个人 token,或者留空,在第 2 步生成访客 token。
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://lab-log.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://lab-log.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// lab-log.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://lab-log.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://lab-log.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// LabLog.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class LabLog {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://lab-log.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://lab-log.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://lab-log.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://lab-log.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
第 2 步 — 一个 token
访客 token 够调用 /me 和 /estimate。运行是计量的,需要个人 token:在 /tokens.html 登录,从那里复制。
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"lab-log"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "lab-log"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "lab-log" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "lab-log"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "lab-log"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "lab-log" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "lab-log"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "lab-log" }))!["token"]!.GetValue<string>();
第 3 步 — GET /me
token 属于谁(subject_type:user 或 guest),以及额度余额。
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
第 4 步 — POST /estimate
免费。请求体就是运行输入本身,一个裸对象(从不包在 {"input": ...} 里)。hold_credits 是预留额度,不是价格;min_credits 是余额至少要有的;model_alias 是 gpt-terra。
# input.json: the run input object - {"task": "log", "entry": "标题:... | 实验 ID:... | 页面写好的 frontmatter ...", "notes": "原始笔记 ...", "facts": "页面结论:... | 待确认 Q1 ... | must: ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "log", "entry": "标题:... | 实验 ID:... | 页面写好的 frontmatter ...", "notes": "原始笔记 ...", "facts": "页面结论:... | 待确认 Q1 ... | must: ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "log", entry, notes, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
第 5 步 — POST /run,然后轮询任务
用通道和输入做一个 Idempotency-Key:同一个键的重试从不重复收费。页面用的是 lab-log:<task>:<inputHash>:a<attempt> - 通道是键的一部分,retry_note 不算进哈希 - 它唯一的一次自动格式重试换一个新的尝试号,在页面上明说是多一次运行。log 的回复带 summary、sections、questions、tags 和 caveats。下面的代码打印它的要点:一句话的 summary,再按顺序打印每个问题(页面的待确认行在前)。
KEY="lab-log:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.summary, (.questions | to_entries[] | "\(.key + 1). \(.value)")' reply.json
import hashlib, time
key = "lab-log:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
print(reply["summary"])
for i, q in enumerate(reply["questions"], 1):
print(i, q)
import { createHash } from "node:crypto";
const key = `lab-log:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
console.log(reply.summary);
reply.questions.forEach((q, i) => console.log(i + 1, q));
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "lab-log:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
fmt.Println(reply["summary"])
qs, _ := reply["questions"].([]any)
for i, q := range qs {
fmt.Println(i+1, q)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "lab-log:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
System.out.println(reply.path("summary").asText());
int i = 1;
for (JsonNode q : reply.path("questions")) System.out.println((i++) + " " + q.asText());
require "digest"
key = "lab-log:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
puts reply["summary"]
reply["questions"].each_with_index { |q, i| puts "#{i + 1} #{q}" }
$key = "lab-log:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
echo $reply["summary"], "\n";
foreach ($reply["questions"] as $i => $q) echo $i + 1, " ", $q, "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"lab-log:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
Console.WriteLine($"{j["charged_credits"]} {reply["task"]}");
Console.WriteLine(reply["summary"]);
var i = 1;
foreach (var q in reply["questions"]!.AsArray()) Console.WriteLine($"{i++} {q}");
第 6 步 — POST /run-stream
同一次运行,用 Server-Sent Events,同一个 input.json。浏览器收到的是进度信号而不是文字片段;最后的 done 事件带着回复。
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: lab-log:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "log", "entry": "标题:... | 实验 ID:... | 页面写好的 frontmatter ...", "notes": "原始笔记 ...", "facts": "页面结论:... | 待确认 Q1 ... | must: ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "lab-log:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `lab-log:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "lab-log:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "lab-log:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "lab-log:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: lab-log:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"lab-log:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);