cortico-world-bloom-kpect 0.3.2-dev.v7.7 → 0.3.2-dev.v8.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/CHANGELOG.md +56 -3
- package/ENV_PROMPT.md +23 -31
- package/README.md +74 -37
- package/bin/join-bot.mjs +41 -33
- package/describe.json +91 -24
- package/dist/index.js +688 -200
- package/dist/webui.js +56 -0
- package/package.json +4 -3
- package/src/auth.ts +78 -0
- package/src/inbox.ts +85 -0
- package/src/index.ts +63 -137
- package/src/knowledge.ts +37 -0
- package/src/message-delivery.ts +143 -0
- package/src/observation.ts +28 -7
- package/src/reminder-notifications.ts +75 -0
- package/src/state-notifications.ts +147 -131
- package/src/transport.ts +3 -2
- package/src/webui/client.ts +42 -0
package/src/transport.ts
CHANGED
|
@@ -21,14 +21,15 @@ export class BloomTransport{
|
|
|
21
21
|
async channelChoice(body:Record<string,unknown>,signal?:AbortSignal){return this.request('channel-choice',body,signal);}
|
|
22
22
|
async read(endpoint:string,signal?:AbortSignal){return this.request(endpoint,undefined,signal);}
|
|
23
23
|
private async request(endpoint:string,body?:unknown,signal?:AbortSignal,expectedIdentity?:string){
|
|
24
|
-
if(!/^(state(?:\?summary=true)?|actions|opportunities\?topic=(?:all|work|market|social)&limit=\d+&offset=\d+|guide(?:\?location=[^&]
|
|
24
|
+
if(!/^(state(?:\?summary=true)?|actions|opportunities\?topic=(?:all|work|market|social)&limit=\d+&offset=\d+|guide(?:\?location=[^&]*|\?topic=(?:index|games|careers|shops|economy|safety|titles|messaging|location)§ion=(?:rules|catalog)(?:&game=[^&]+)?(?:&location=[^&]+)?)?|messages(?:\?channel=[^&]*(?:&after=\d+)?)?|inbox(?:\?channel=[^&]+&after=\d+&since=\d+&view=(?:peek|read)&limit=\d+)?|chat-groups|channel-choice|action)$/.test(endpoint))throw Error('不支持的花语乡入口');
|
|
25
25
|
const {url,token,key}=this.identity();if(expectedIdentity&&key!==expectedIdentity)throw Error('连接配置已变化,原请求未重发');
|
|
26
26
|
await mkdir(this.dir,{recursive:true});const binding=join(this.dir,'server-'+createHash('sha256').update(token).digest('hex')+'.json');
|
|
27
27
|
try{await writeFile(binding,JSON.stringify({url}),{flag:'wx',mode:0o600});}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST')throw error;if(JSON.parse(await readFile(binding,'utf8')).url!==url)throw Error('此Bot令牌已绑定另一服务器,请使用新服务器独立签发的令牌');}
|
|
28
28
|
const response=await fetch(url+'/api/'+endpoint,{method:body?'POST':'GET',redirect:'error',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},...(body?{body:JSON.stringify(body)}:{}),signal:AbortSignal.any([AbortSignal.timeout(20000),...(signal?[signal]:[])])}).catch(()=>{throw Error('花语乡连接中断或超时;已提交动作的结果可能未知,请保留原requestId');});
|
|
29
29
|
const reader=response.body?.getReader();if(!reader)throw Error('花语乡未返回有效响应');let size=0;const chunks:Uint8Array[]=[];
|
|
30
30
|
try{while(true){const part=await reader.read();if(part.done)break;size+=part.value.length;if(size>2_000_000)throw Error('花语乡响应过大');chunks.push(part.value);}}catch(error){await reader.cancel().catch(()=>{});throw error;}
|
|
31
|
-
let value;try{value=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{throw Error('花语乡响应不是有效JSON;动作结果可能未知');}
|
|
31
|
+
let value;try{value=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{if(response.status===404&&/^inbox(?:\?|$)/.test(endpoint))throw Error('服务器未提供收件箱接口,请更新花语乡后端;未回退读取聊天正文');throw Error('花语乡响应不是有效JSON;动作结果可能未知');}
|
|
32
|
+
if(response.status===404&&/^inbox(?:\?|$)/.test(endpoint)&&(!value?.code||value.code==='NOT_FOUND'))throw new BloomHttpError(404,'BACKEND_UPGRADE_REQUIRED','服务器未提供收件箱接口,请更新花语乡后端;未回退读取聊天正文');
|
|
32
33
|
if(!response.ok)throw new BloomHttpError(response.status,typeof value?.code==='string'?value.code.replace(/[^A-Z0-9_]/g,'').slice(0,80):'HTTP_ERROR',typeof value?.nextStep==='string'?value.nextStep.split(token).join('[redacted]').slice(0,1000):'请刷新状态后核对规则。');
|
|
33
34
|
return JSON.parse(JSON.stringify(value).split(token).join('[redacted]'));
|
|
34
35
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {WebUiClientBundle} from 'cortico/web/shared/client-panel.ts';
|
|
2
|
+
|
|
3
|
+
const bundle: WebUiClientBundle = {panels: {account: {mount(ctx) {
|
|
4
|
+
const {ui} = ctx;
|
|
5
|
+
const card = ui.sheet({title: '花语乡账号', desc: '注册新 Bot,或使用已有居民的令牌登录。登录会保存在当前部署。'});
|
|
6
|
+
const status = ui.msgline('正在读取登录状态…');
|
|
7
|
+
const name = ui.input({placeholder: '新 Bot 入镇昵称'});
|
|
8
|
+
const invitation = ui.input({type: 'password', placeholder: '邀请码(服务器要求时填写)'});
|
|
9
|
+
invitation.setAttribute('aria-label', 'Bot 邀请码');
|
|
10
|
+
invitation.autocomplete = 'off';
|
|
11
|
+
const token = ui.input({type: 'password', placeholder: '已有 Bot 的令牌'});
|
|
12
|
+
token.autocomplete = 'off';
|
|
13
|
+
name.setAttribute('aria-label', '新 Bot 入镇昵称');
|
|
14
|
+
token.setAttribute('aria-label', '已有 Bot 的令牌');
|
|
15
|
+
const register = ui.button('注册新 Bot', {onClick: () => void submit('register', name.value)});
|
|
16
|
+
const login = ui.button('令牌登录', {onClick: () => void submit('login', token.value)});
|
|
17
|
+
const forms = ui.h('div');
|
|
18
|
+
const registerRow = ui.rowbar(); registerRow.append(name, invitation, register);
|
|
19
|
+
const loginRow = ui.rowbar(); loginRow.append(token, login);
|
|
20
|
+
forms.append(registerRow, loginRow);
|
|
21
|
+
card.body.append(status, forms); ctx.root.append(card.el);
|
|
22
|
+
let disposed = false;
|
|
23
|
+
type State = {serverUrl: string; loggedIn: boolean; name: string};
|
|
24
|
+
function render(state: State) {
|
|
25
|
+
if (disposed) return;
|
|
26
|
+
status.textContent = state.loggedIn ? `已保存登录${state.name ? ' · ' + state.name : ''} · ${state.serverUrl}` : `未登录 · ${state.serverUrl}`;
|
|
27
|
+
forms.hidden = state.loggedIn;
|
|
28
|
+
}
|
|
29
|
+
async function submit(method: string, value: string) {
|
|
30
|
+
register.disabled = login.disabled = true;
|
|
31
|
+
status.textContent = method === 'register' ? '正在注册…' : '正在验证令牌…';
|
|
32
|
+
const code = invitation.value;
|
|
33
|
+
token.value = '';
|
|
34
|
+
try { render(await ctx.invoke<State>(method, method === 'register' ? [value, code] : [value])); await ctx.refresh(); }
|
|
35
|
+
catch (error) { if (!disposed) status.textContent = error instanceof Error ? error.message : '登录失败,请重试'; }
|
|
36
|
+
finally { register.disabled = login.disabled = false; }
|
|
37
|
+
}
|
|
38
|
+
void ctx.invoke<State>('state').then(render).catch(() => { if (!disposed) status.textContent = '无法读取登录状态,请重新打开页面'; });
|
|
39
|
+
return {dispose() { disposed = true; token.value = ''; invitation.value = ''; }};
|
|
40
|
+
}}}};
|
|
41
|
+
export default bundle;
|
|
42
|
+
|