@ai.weget.jp/bot 0.1.8 → 0.1.9
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 +13 -4
- package/dist/src/config/systemConfig.js +1 -0
- package/dist/src/ipc/registerHandlers.js +1 -1
- package/dist/src/main.js +1 -1
- package/dist/src/preload.cjs +26 -8
- package/dist/src/renderer/app.js +1 -1
- package/dist/src/renderer/index.html +317 -13
- package/dist/src/renderer/styles.css +451 -2
- package/dist/src/services/aiChatService.js +1 -0
- package/dist/src/services/authApiService.js +1 -1
- package/dist/src/services/trade/gmoCoinGateway.js +1 -0
- package/dist/src/services/trade/gmoKlineService.js +1 -0
- package/dist/src/services/trade/gmoWindowService.js +1 -0
- package/dist/src/services/trade/tradeConfigService.js +1 -0
- package/dist/src/services/trade/tradeStatusService.js +1 -0
- package/dist/src/services/trade/types.js +1 -0
- package/dist/src/services/windowManagerService.js +1 -1
- package/dist/src/wsClient.js +1 -1
- package/package.json +1 -7
- package/dist/src/renderer/chat.css +0 -128
- package/dist/src/renderer/chat.html +0 -27
- package/dist/src/renderer/chat.js +0 -1
- package/dist/src/services/aiMemoryService.js +0 -1
- package/dist/src/services/commandOrchestratorService.js +0 -1
- package/dist/src/services/debugLogService.js +0 -1
- package/dist/src/services/taskExecutorService.js +0 -1
- package/dist/src/services/taskParsingService.js +0 -1
- package/dist/src/services/textService.js +0 -1
- package/dist/src/services/workflowService.js +0 -1
- package/scripts/backtest-loop.cjs +0 -137
- package/scripts/prepare-playwright-browsers.cjs +0 -181
- package/scripts/run-workflow.cjs +0 -381
- package/scripts/workflows/kaitori-login.json +0 -17
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
margin: 0;
|
|
3
|
-
font-family: "Segoe UI", sans-serif;
|
|
4
|
-
background: #edf1f8;
|
|
5
|
-
color: #142033;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.chat-app {
|
|
9
|
-
max-width: 980px;
|
|
10
|
-
margin: 0 auto;
|
|
11
|
-
padding: 14px;
|
|
12
|
-
display: grid;
|
|
13
|
-
gap: 12px;
|
|
14
|
-
min-height: 100vh;
|
|
15
|
-
box-sizing: border-box;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.chat-header {
|
|
19
|
-
background: #fff;
|
|
20
|
-
border-radius: 12px;
|
|
21
|
-
padding: 12px 14px;
|
|
22
|
-
box-shadow: 0 8px 20px rgba(19, 30, 49, 0.08);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.chat-header h1 {
|
|
26
|
-
margin: 0 0 4px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.chat-header p {
|
|
30
|
-
margin: 0;
|
|
31
|
-
color: #61718c;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.chat-header-row {
|
|
35
|
-
display: flex;
|
|
36
|
-
justify-content: space-between;
|
|
37
|
-
align-items: flex-start;
|
|
38
|
-
gap: 12px;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.memory-tools {
|
|
42
|
-
display: flex;
|
|
43
|
-
gap: 8px;
|
|
44
|
-
align-items: center;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.memory-tools select {
|
|
48
|
-
border: 1px solid #c7d3e7;
|
|
49
|
-
border-radius: 8px;
|
|
50
|
-
padding: 8px 10px;
|
|
51
|
-
background: #fff;
|
|
52
|
-
min-width: 170px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.messages {
|
|
56
|
-
background: #fff;
|
|
57
|
-
border-radius: 12px;
|
|
58
|
-
padding: 12px;
|
|
59
|
-
box-shadow: 0 8px 20px rgba(19, 30, 49, 0.08);
|
|
60
|
-
overflow: auto;
|
|
61
|
-
min-height: 420px;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.msg {
|
|
65
|
-
margin-bottom: 10px;
|
|
66
|
-
padding: 10px;
|
|
67
|
-
border-radius: 10px;
|
|
68
|
-
white-space: pre-wrap;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.msg.user {
|
|
72
|
-
background: #dce9ff;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.msg.assistant {
|
|
76
|
-
background: #f2f4f8;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.msg.line-user {
|
|
80
|
-
background: #e6f7ee;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.msg.line-assistant {
|
|
84
|
-
background: #fff7e6;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.composer {
|
|
88
|
-
background: #fff;
|
|
89
|
-
border-radius: 12px;
|
|
90
|
-
padding: 12px;
|
|
91
|
-
box-shadow: 0 8px 20px rgba(19, 30, 49, 0.08);
|
|
92
|
-
display: grid;
|
|
93
|
-
gap: 8px;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
textarea {
|
|
97
|
-
width: 100%;
|
|
98
|
-
box-sizing: border-box;
|
|
99
|
-
border: 1px solid #c7d3e7;
|
|
100
|
-
border-radius: 8px;
|
|
101
|
-
padding: 10px 12px;
|
|
102
|
-
font-family: inherit;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
button {
|
|
106
|
-
justify-self: end;
|
|
107
|
-
border: none;
|
|
108
|
-
background: #1e56ce;
|
|
109
|
-
color: #fff;
|
|
110
|
-
border-radius: 8px;
|
|
111
|
-
padding: 9px 14px;
|
|
112
|
-
cursor: pointer;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
#upload-memory-btn {
|
|
116
|
-
background: #0f766e;
|
|
117
|
-
white-space: nowrap;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
#download-memory-btn {
|
|
121
|
-
background: #3b5ccc;
|
|
122
|
-
white-space: nowrap;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
button:disabled {
|
|
126
|
-
opacity: 0.7;
|
|
127
|
-
cursor: not-allowed;
|
|
128
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Bot Chat</title>
|
|
7
|
-
<link rel="stylesheet" href="./chat.css" />
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<main class="chat-app">
|
|
11
|
-
<header class="chat-header">
|
|
12
|
-
<div class="chat-header-row">
|
|
13
|
-
<div>
|
|
14
|
-
<h1>Bot Chat</h1>
|
|
15
|
-
<p>ChatGPT API key is fetched from login API response.</p>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
</header>
|
|
19
|
-
<section id="messages" class="messages"></section>
|
|
20
|
-
<form id="chat-form" class="composer">
|
|
21
|
-
<textarea id="chat-input" rows="3" placeholder="Type your message..." required></textarea>
|
|
22
|
-
<button id="send-btn" type="submit">Send</button>
|
|
23
|
-
</form>
|
|
24
|
-
</main>
|
|
25
|
-
<script type="module" src="./chat.js"></script>
|
|
26
|
-
</body>
|
|
27
|
-
</html>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=document.getElementById("chat-form"),t=document.getElementById("chat-input"),n=document.getElementById("messages"),s=document.getElementById("send-btn"),i=(e,t,s="")=>{const i=document.createElement("div");i.className=`msg ${e}`;let a=s||("user"===e?"You":"Assistant");"line-user"===e&&(a=s||"LINE User"),"line-assistant"===e&&(a=s||"AI"),i.textContent=`${a}: ${t}`,n.appendChild(i),n.scrollTop=n.scrollHeight};e.addEventListener("submit",async e=>{e.preventDefault();const n=t.value.trim();if(!n)return;i("user",n),t.value="",s.disabled=!0;const a=await window.botApi.sendChat(n);a.ok?i("assistant",String(a.answer||"")):i("assistant",`Error: ${a.error}`),s.disabled=!1}),window.botApi.onChatEvent(e=>{if(e){if("line_user"===e.type){const t=e.lineUserId?`LINE(${e.lineUserId})`:"LINE";return void i("line-user",e.text||"",t)}if("line_assistant"===e.type){const t=e.lineUserId?`AI->LINE(${e.lineUserId})`:"AI->LINE";i("line-assistant",e.text||"",t)}}});export{};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import e from"node:crypto";import t from"openai";const r=e=>e&&"object"==typeof e?e:{},n=e=>String(r(e).id||"").trim();export const createAiMemoryService=({env:o,emitLog:i,getCurrentSession:s,setCurrentSession:a,callBotApi:u,getBotMemoryUploadApiUrl:d,getBotMemoryArchivesApiUrl:c,getBotMemoryDownloadApiUrl:p,getBotRuntimeConfigApiUrl:m,loadLocalMemories:l,saveLocalMemories:g,cosineSimilarity:y,parseJsonLoose:h,buildLanguageInstruction:A})=>{let b=null,f="";const _=e=>{const t=r(e);if("string"==typeof t.output_text&&t.output_text)return t.output_text;const n=[],o=Array.isArray(t.output)?t.output:[];for(const e of o){const t=r(e),o=Array.isArray(t.content)?t.content:[];for(const e of o){const t=r(e);"output_text"===t.type&&"string"==typeof t.text&&t.text&&n.push(t.text),"text"===t.type&&"string"==typeof t.text&&t.text&&n.push(t.text),"string"==typeof t.text&&t.text&&n.push(t.text),"string"==typeof t.output_text&&t.output_text&&n.push(t.output_text)}}return n.join("\n").trim()},w=()=>{const e=s();if(!e?.chatApiKey)throw new Error("Login API did not return chat_api_key");return b&&f===e.chatApiKey||(b=new t({apiKey:e.chatApiKey}),f=e.chatApiKey),b},S=async({purpose:e,request:t})=>{i("[openai] responses request",{purpose:e,model:t.model||"",instructions:t.instructions||"",previous_response_id:t.previous_response_id||"",max_output_tokens:t.max_output_tokens??null,input:t.input||[]});const o=await w().responses.create(t);return i("[openai] responses response",{purpose:e,response_id:n(o),output_text:_(o)||"",output:r(o).output||[],usage:r(o).usage||null}),o},x=async e=>{if(!o.memoryEmbeddingsEnabled)return[];const t=String(e||"").trim();if(!t)return[];const r=await(async({purpose:e,request:t})=>{const r=await w().embeddings.create(t);return o.memoryEmbeddingsEnabled&&i("[openai] embeddings response",{purpose:e,model:t.model||"",vector_size:Array.isArray(r.data)&&r.data[0]?.embedding?r.data[0].embedding.length:0,usage:r.usage||null}),r})({purpose:"memory-embedding",request:{model:o.memoryEmbeddingModel||"text-embedding-3-small",input:t}});return Array.isArray(r?.data)&&r.data[0]?.embedding?r.data[0].embedding:[]};return{parseTaskPlan:async(e,t,n={})=>{const i=Boolean(n.forceAction),a=s(),u=[String(o.plannerInstructions||"").trim(),`User preferred language: ${t}.`,i?"Must prefer mode=action where possible.":"","If assumptions are used, write short assumptions array.","No markdown. No extra text."].filter(Boolean).join(" "),d=await S({purpose:"task-plan",request:{model:a?.chatModel||"gpt-4.1-mini",instructions:u,max_output_tokens:Math.max(o.parseMaxOutputTokens,220),input:[{role:"user",content:[{type:"input_text",text:e}]}]}}),c=_(d),p=h(c),m=r(p);if(0===Object.keys(m).length)return{mode:"chat",confidence:0,actions:[],assumptions:[],response:"",raw:c};const l=String(m.mode||"").trim();return{mode:"action"===l||"chat"===l?l:"chat",confidence:Number(m.confidence||0),actions:Array.isArray(m.actions)?m.actions:[],assumptions:Array.isArray(m.assumptions)?m.assumptions.map(e=>String(e||"").trim()).filter(Boolean):[],response:String(m.response||"").trim(),raw:c}},sendChatMessage:async(e,t={})=>{const r=s(),i=[r?.chatSystemPrompt||"",t.languageInstruction||"","Keep response concise and practical.","Maximum output length: about 50 Chinese characters OR 80 English words."].filter(Boolean).join("\n"),a=await S({purpose:"chat",request:{model:r?.chatModel||"gpt-4.1-mini",instructions:i||void 0,previous_response_id:t.previousResponseId||void 0,max_output_tokens:o.maxOutputTokens,input:[{role:"user",content:[{type:"input_text",text:e}]}]}});return{text:_(a)||"",responseId:n(a)}},summarizeExecutionResult:async({userText:e,language:t,execution:r,previousResponseId:i})=>{const a=s(),u=await S({purpose:"execution-summary",request:{model:a?.chatModel||"gpt-4.1-mini",instructions:[A(t),"Summarize task result for end user in concise style.","Use max 80 words or around 50 Chinese characters.","If failed, clearly state failed step and suggest retry."].join("\n"),previous_response_id:i||void 0,max_output_tokens:o.maxOutputTokens,input:[{role:"user",content:[{type:"input_text",text:JSON.stringify({request:e,execution:r})}]}]}});return{text:_(u)||"",responseId:n(u)}},findBestMemoryMatch:async e=>{const t=await l();if(!t.length)return null;const r=await x(e);if(!r.length)return null;let n=null;for(const e of t){const t=y(r,e.embedding||[]);(!n||t>n.score)&&(n={item:e,score:t})}return n?n.score<Number(o.memorySimilarityThreshold??.82)?null:n:null},rememberResolvedAction:async({queryText:t,actions:r,language:n})=>{if(!t||!Array.isArray(r)||0===r.length)return;const o=await x(t);if(!o.length)return;const i=s(),a=await l();a.push({id:e.randomUUID(),userId:i?.userId||"",botId:i?.botId||"",query:t,language:n||"zh",actions:r,embedding:o,hits:0,isArchived:!1,archiveName:null,archivedAt:null,syncedAt:null,createdAt:(new Date).toISOString(),updatedAt:(new Date).toISOString()});const u=a.sort((e,t)=>String(t.updatedAt||"").localeCompare(String(e.updatedAt||""))).slice(0,500);await g(u)},markMemoryUsed:async e=>{if(!e)return;const t=await l(),r=t.find(t=>t.id===e);r&&(r.hits=Number(r.hits||0)+1,r.updatedAt=(new Date).toISOString(),await g(t))},uploadLocalMemoriesToSupabase:async()=>{if(!o.memoryEmbeddingsEnabled)return{uploaded:0,total:0,disabled:!0};const e=s();if(!e?.userId)throw new Error("bot session user_id missing");const t=await l();if(!t.length)return{uploaded:0,total:0};const r=t.filter(t=>t.userId===e.userId&&(!t.syncedAt||t.syncedAt<t.updatedAt));if(!r.length)return{uploaded:0,total:t.length};const n=r.map(t=>({id:t.id,botId:t.botId||e.botId||"",query:t.query||"",language:t.language||"zh",actions:Array.isArray(t.actions)?t.actions:[],embedding:Array.isArray(t.embedding)?t.embedding:[],hits:Number(t.hits||0),isArchived:Boolean(t.isArchived||!1),archiveName:t.archiveName||null,archivedAt:t.archivedAt||null,updatedAt:t.updatedAt||(new Date).toISOString()}));await u(d(),{bot_id:e.botId||"",memories:n});const i=(new Date).toISOString(),a=new Set(r.map(e=>e.id));for(const e of t)a.has(e.id)&&(e.syncedAt=i);return await g(t),{uploaded:r.length,total:t.length}},listArchivedMemoryGroupsFromSupabase:async()=>{const e=s();if(!e?.userId)throw new Error("bot session user_id missing");const t=await u(c(),{bot_id:e.botId||""});return Array.isArray(t?.groups)?t.groups:[]},downloadArchivedMemoriesFromSupabase:async e=>{const t=s();if(!t?.userId)throw new Error("bot session user_id missing");const n=String(e||"").trim();if(!n)throw new Error("archive name is required");const o=await u(p(),{bot_id:t.botId||"",archive_name:n}),i=Array.isArray(o.memories)?o.memories:[],a=await l(),d=new Map(a.map(e=>[e.id,e]));for(const e of i){const n=r(e),o=String(n.id||"").trim();o&&d.set(o,{id:o,userId:String(n.userId||n.user_id||t.userId||""),botId:String(n.botId||n.bot_id||t.botId||""),query:String(n.query||""),language:String(n.language||"zh"),actions:Array.isArray(n.actions)?n.actions:[],embedding:Array.isArray(n.embedding)?n.embedding.map(e=>Number(e||0)):[],hits:Number(n.hits||0),isArchived:Boolean(n.isArchived||n.is_archived||!1),archiveName:n.archiveName?String(n.archiveName):n.archive_name?String(n.archive_name):null,archivedAt:n.archivedAt?String(n.archivedAt):n.archived_at?String(n.archived_at):null,syncedAt:n.syncedAt?String(n.syncedAt):n.synced_at?String(n.synced_at):null,createdAt:String(n.createdAt||n.created_at||(new Date).toISOString()),updatedAt:String(n.updatedAt||n.updated_at||(new Date).toISOString())})}return await g(Array.from(d.values())),{downloaded:i.length,archiveName:n}},refreshRuntimeConfigFromServer:async()=>{const e=s();if(!e?.botId)return{ok:!1,reason:"missing bot session"};const t=await u(m(),{bot_id:e.botId}),r=String(t?.chat_api_key||"").trim(),n=String(t?.chat_model||"").trim()||"gpt-4.1-mini";if(!r)throw new Error("runtime config returned empty chat_api_key");return a({...e,chatApiKey:r,chatModel:n}),i("[bot] runtime config refreshed",{chatModel:n}),{ok:!0,chatModel:n}}}};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import e from"node:crypto";const t=e=>e&&"object"==typeof e?e:{},r=e=>(Array.isArray(e)?e:[]).map(e=>t(e)).map(e=>({type:String(e.type||"").trim(),params:e.params&&"object"==typeof e.params?e.params:{}})).filter(e=>Boolean(e.type));export const createCommandOrchestratorService=({getCurrentSession:s,setBotBusyState:o,emitChatEvent:n,emitLog:a,emitTaskEvent:i,wsSendLineReply:l,normalizeIncomingText:p,isNumericChoice:d,detectLanguage:w,isLikelyMojibake:c,buildMojibakeReply:u,buildActionFailedReply:f,buildEmptyReply:g,buildCancelReply:m,buildConfirmPrompt:k,buildLanguageInstruction:_,isConfirmText:y,isCancelText:I,matchWorkflowByCommand:h,isLikelyNewTask:x,buildStepStartReply:S,buildStepFallbackSummary:v,extractFirstUrl:T,extractBasicCredentials:U,isBasicAuthStep:A,parseTaskPlan:b,executeActionPlan:L,summarizeExecutionResult:R,sendChatMessage:C,refreshRuntimeConfigFromServer:D,loadWorkflowsFromServer:$,normalizeWorkflow:O,resolveTaskFallbackUrl:E,taskExecutorResetWorkflowState:N,emitDebugEvent:B})=>{const j=new Map;let P=0;const W=new Map,M=(e="line_message_processing")=>{P+=1,1===P&&o(!0,e)},F=(e="line_message_completed")=>{P=Math.max(0,P-1),0===P&&o(!1,e)},q=e=>(j.has(e)||j.set(e,{threadResponseId:"",preferredLanguage:"zh",lastTask:null,workflows:[],workflowCacheTs:0,pendingWorkflow:null}),j.get(e)),z=({lineUserId:e,text:t,imageUrl:r,previewImageUrl:o})=>{const n=s();n?.botId&&e&&(t||r)&&l({action:"line_reply",bot_id:n.botId,user_id:n.userId||void 0,line_user_id:e,text:t||void 0,image_url:r||void 0,preview_image_url:o||void 0,ts:(new Date).toISOString()})},K=({verify:e,execution:t})=>{if(!e||"object"!=typeof e)return{ok:!0,type:"none",expected:"",actual:"",reason:"verify_not_provided"};const r=String(e.type||"").trim().toLowerCase(),s=String(e.value||"").trim();if("action_ok"===r){const e=(t?.logs||[]).find(e=>!e?.ok);return{ok:!e,type:r,expected:"all_actions_ok",actual:e?String(e.type||"unknown"):"all_actions_ok",reason:e?"action_failed":"ok"}}if("url_contains"===r){const e=(e=>{const t=Array.isArray(e?.logs)?e.logs:[];for(let e=t.length-1;e>=0;e-=1){const r=t[e];if("open_url"===r?.type&&r?.ok&&"string"==typeof r?.url)return r}return null})(t),o=String(e?.url||"").trim(),n=Boolean(s)&&o.includes(s);return{ok:n,type:r,expected:s,actual:o,reason:n?"ok":"url_not_matched"}}if("text_contains"===r){const e=(e=>(e.logs||[]).find(e=>"read_page_text"===e?.type&&e?.ok&&"string"==typeof e?.text)||null)(t),o=String(e?.text||""),n=Boolean(s)&&o.toLowerCase().includes(s.toLowerCase());return{ok:n,type:r,expected:s,actual:o.slice(0,200),reason:n?"ok":"text_not_matched"}}return{ok:!1,type:r||"unknown",expected:s,actual:"",reason:"unsupported_verify_type"}},V=async({workflow:t,lineText:s,lineSession:o,lineUserId:n,runId:i})=>{const l=Array.isArray(t?.steps)?t.steps:[];if(!l.length)return{answer:f(o.preferredLanguage),imageUrl:"",previewImageUrl:""};N();const p=String(i||e.randomUUID());B({level:"info",phase:"request",event:"workflow_started",runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepIndex:0,stepTotal:l.length,stepTitle:"",data:{line_text:s,step_total:l.length,line_user_id:n||""}});const d=[];let w="",c="";for(let e=0;e<l.length;e+=1){const i=l[e],u=i.title||`Step ${e+1}`;a("[workflow] step started",{workflowId:t.workflowId||"",workflowName:t.name||"",stepTitle:u,workflowStepIndex:e+1,workflowStepTotal:l.length}),B({level:"info",phase:"step_start",event:"step_started",runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepIndex:e+1,stepTotal:l.length,stepTitle:u,data:{step_instruction:i.instruction||""}}),z({lineUserId:n,text:S(o.preferredLanguage,e+1,l.length,u)});const g=d.map(e=>`- ${e.title}: ${e.summary}`).join("\n"),m=String(i.instruction||"").replaceAll("{{user_request}}",s),k=[`Workflow: ${t.name}`,`User request: ${s}`,g?`Previous step summaries:\n${g}`:"",`Current step: ${u}`,`Step instruction: ${m}`].filter(Boolean).join("\n"),_=T(m),y=_?null:U(m),I=A(u,m),h=_?(()=>{const e=U(m),t=[];return e&&t.push({type:"set_http_credentials",params:{username:e.username,password:e.password,host:e.host||""}}),t.push({type:"open_url",params:{url:_}}),t.push({type:"wait",params:{ms:1200}}),{mode:"action",confidence:1,actions:t,assumptions:e?["step contains explicit URL and basic auth credentials; apply credentials then open_url"]:["step contains explicit URL; use deterministic open_url"],response:""}})():y&&I?(()=>{const e=E(),t=[{type:"set_http_credentials",params:{username:y.username,password:y.password,host:y.host||""}}];return e&&(t.push({type:"open_url",params:{url:e}}),t.push({type:"wait",params:{ms:1200}})),{mode:"action",confidence:1,actions:t,assumptions:e?["step contains only basic auth credentials; re-open last URL after applying credentials"]:["step contains only basic auth credentials; no valid URL available to reopen"],response:""}})():y?{mode:"action",confidence:.98,actions:[{type:"auto_login_form",params:{username:y.username,password:y.password}},{type:"wait",params:{ms:1200}}],assumptions:["step contains username/password for normal form login"],response:""}:await b(k,o.preferredLanguage,{forceAction:!0});if(a("[workflow] step parsed plan",{workflowId:t.workflowId||"",workflowName:t.name||"",stepTitle:u,workflowStepIndex:e+1,workflowStepTotal:l.length,plan:h}),B({level:"info",phase:"plan",event:"plan_generated",runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepIndex:e+1,stepTotal:l.length,stepTitle:u,data:{plan:h}}),"action"!==h.mode||!Array.isArray(h.actions)||0===h.actions.length){const t=h.response||f(o.preferredLanguage);throw z({lineUserId:n,text:t}),new Error(`workflow step ${e+1} not executable`)}const x=await L(r(h.actions),{runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepTitle:u,stepIndex:e+1,stepTotal:l.length}),C=await R({userText:k,language:o.preferredLanguage,execution:x,previousResponseId:o.threadResponseId});o.threadResponseId=C.responseId||o.threadResponseId;const D=C.text||v(o.preferredLanguage,e+1,l.length,u);d.push({title:u,summary:D}),w=x.firstImageUrl||w,c=x.firstImagePreviewUrl||c,z({lineUserId:n,text:D,imageUrl:x.firstImageUrl||"",previewImageUrl:x.firstImagePreviewUrl||x.firstImageUrl||""}),a("[workflow] step completed",{workflowId:t.workflowId||"",workflowName:t.name||"",stepTitle:u,workflowStepIndex:e+1,workflowStepTotal:l.length}),B({level:"info",phase:"step_end",event:"step_completed",runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepIndex:e+1,stepTotal:l.length,stepTitle:u,data:{step_summary:D,image_url:x.firstImageUrl||"",artifacts:Array.isArray(x.artifacts)?x.artifacts.length:0,logs:Array.isArray(x.logs)?x.logs.length:0}})}const u=d.map((e,t)=>`${t+1}. ${e.summary}`).join("\n");return B({level:"info",phase:"summary",event:"workflow_completed",runId:p,workflowId:t.workflowId||"",workflowName:t.name||"",stepIndex:l.length,stepTotal:l.length,stepTitle:"",data:{final_summary:u||""}}),{answer:u||g(o.preferredLanguage),imageUrl:w,previewImageUrl:c}};return{getLineSession:q,clearLineSessions:()=>{j.clear()},executeWorkflowBySteps:V,handleLineMessage:async t=>{const o=String(t?.text||"").trim(),i=p(o),l=String(t?.line_user_id||"");if(!i)return{answer:""};if(!s())throw new Error("bot session not ready");o&&o!==i&&a("[text] normalized incoming text encoding",{before:o,after:i});const S=q(l);if(d(i)||(S.preferredLanguage=w(i)||S.preferredLanguage),c(i)){a("[text] likely mojibake detected",{text:i});const e=u(S.preferredLanguage);return n({type:"line_assistant",text:e,lineUserId:l,ts:(new Date).toISOString()}),{answer:e}}n({type:"line_user",text:i,lineUserId:l,ts:(new Date).toISOString()}),M("line_message_processing");let v="",U="",A="";try{const t=async e=>{const t=await b(e,S.preferredLanguage,{forceAction:!0});if(a("[task] parsed plan",t),"chat"===t.mode)return{answer:t.response||f(S.preferredLanguage)};if(!Array.isArray(t.actions)||0===t.actions.length)return{answer:f(S.preferredLanguage)};const s=await L(r(t.actions)),o=await R({userText:e,language:S.preferredLanguage,execution:s,previousResponseId:S.threadResponseId});return S.threadResponseId=o.responseId||S.threadResponseId,S.lastTask={requestText:e,answerText:o.text||"",revisionCount:Number(S.lastTask?.revisionCount||0),ts:Date.now()},{answer:o.text||g(S.preferredLanguage),answerImageUrl:s.firstImageUrl||"",answerPreviewImageUrl:s.firstImagePreviewUrl||s.firstImageUrl||""}};try{const r=await(async e=>{const t=Date.now(),r=Number(e.workflowCacheTs||0);if(Array.isArray(e.workflows)&&e.workflows.length>0&&t-r<6e4)return e.workflows;const s=await $();return e.workflows=s,e.workflowCacheTs=t,s})(S),s=S.pendingWorkflow||null;if(s)if(y(i)){S.pendingWorkflow=null;const t=e.randomUUID(),r=await V({workflow:s.workflow,lineText:s.originalText||i,lineSession:S,lineUserId:l,runId:t});v=r.answer,U=r.imageUrl||"",A=r.previewImageUrl||""}else I(i)?(S.pendingWorkflow=null,v=m(S.preferredLanguage)):v=k(S.preferredLanguage,s.workflow);else{const e=T(i),s=Boolean(e),o=/(?:^|\s)(打开|访问|进入|open|visit)\b/i.test(i)||/^https?:\/\/\S+/i.test(i);if(s&&o){const t=await L([{type:"open_url",params:{url:e}},{type:"read_page_text",params:{max_chars:1800}},{type:"screenshot",params:{}}]),r=(t.logs||[]).find(e=>"read_page_text"===e.type&&e.ok&&e.text),s=String(r?.text||"").trim(),o=await C([_(S.preferredLanguage),"Based on the page text below, write a short website introduction.","Do not output URLs or links.","Keep it concise.",s?`Page text:\n${s}`:"No readable page text was extracted."].join("\n"),{previousResponseId:S.threadResponseId,languageInstruction:_(S.preferredLanguage)}).catch(()=>({text:"",responseId:""}));S.threadResponseId=o.responseId||S.threadResponseId;const n=String(o.text||"").replace(/https?:\/\/\S+/gi,"").trim(),a=await R({userText:"Open URL and capture screenshot",language:S.preferredLanguage,execution:{...t,logs:(t.logs||[]).filter(e=>"open_url"!==e.type)},previousResponseId:S.threadResponseId});S.threadResponseId=a.responseId||S.threadResponseId,v=n?`${n}\n${a.text||""}`.trim():a.text||g(S.preferredLanguage),U=t.firstImageUrl||"",A=t.firstImagePreviewUrl||t.firstImageUrl||""}else{const e=h(i,r);if(e){const t=String(e.workflowId||"").trim(),s=r.find(e=>e.workflowId===t)||(D=e,{workflowId:String(D.workflowId||"").trim(),name:String(D.name||"").trim(),triggerCommand:String(D.triggerCommand||"").trim(),description:String(D.description||"").trim(),steps:(Array.isArray(D.steps)?D.steps:[]).map((e,t)=>({id:String(e?.id||`s${t+1}`).trim(),title:String(e?.title||`Step ${t+1}`).trim(),instruction:String(e?.instruction||"").trim()})),isActive:Boolean(D.isActive??!0)});S.pendingWorkflow={workflowId:s.workflowId,workflow:s,originalText:i,createdAt:Date.now()},v=k(S.preferredLanguage,s)}else{const e=Boolean(S.lastTask?.requestText)&&Number(S.lastTask?.revisionCount||0)<3&&!x(i),r=S.lastTask,s=e&&r?[`Previous request: ${r.requestText}`,`Previous result: ${r.answerText||""}`,`User follow-up revision: ${i}`,"Revise plan and execute again to satisfy latest user intent."].join("\n"):i,o=await t(s);v=o.answer,U=o.answerImageUrl||"",A=o.answerPreviewImageUrl||"",S.lastTask&&(S.lastTask.revisionCount=e?Number(S.lastTask.revisionCount||0)+1:0)}}}}catch(e){a("[task] parse/execute failed, fallback to chat",{error:e instanceof Error?e.message:String(e)});const t=await C(i,{previousResponseId:S.threadResponseId,languageInstruction:_(S.preferredLanguage)}).catch(()=>({text:"",responseId:""}));v=t.text||f(S.preferredLanguage),S.threadResponseId=t.responseId||S.threadResponseId}return n({type:"line_assistant",text:v,lineUserId:l,ts:(new Date).toISOString()}),{answer:v,imageUrl:U,previewImageUrl:A}}finally{F("line_message_completed")}var D},handleServerConfigUpdate:async e=>{const t=Array.isArray(e?.scopes)?e.scopes.map(e=>String(e||"").trim()):[];if(a("[bot] applying server_config_update",{scopes:t}),t.includes("workflow"))for(const e of j.values())e.workflows=[],e.workflowCacheTs=0,e.pendingWorkflow=null;t.includes("ai_settings")&&await D()},handleWorkflowExecute:async r=>{if(!s())throw new Error("bot session not ready");const o=String(r?.workflow_id||"").trim();if(!o)throw new Error("workflow_id is required");const n=q("__platform__");n.pendingWorkflow=null;const i=r?.workflow&&"object"==typeof r.workflow?O(r.workflow):null,l=i&&i.workflowId===o?i:null;if(l){n.workflows=[l],n.workflowCacheTs=Date.now();const e=t(r?.workflow);a("[workflow] execute with pushed snapshot",{workflowId:o,updatedAt:String(e.updated_at||""),steps:Array.isArray(l.steps)?l.steps.length:0})}else{const e=await $();n.workflows=e,n.workflowCacheTs=Date.now();const t=e.find(e=>e.workflowId===o);if(!t)return void a("[workflow] execute ignored, workflow not found",{workflowId:o});a("[workflow] execute with server fetch",{workflowId:o,steps:Array.isArray(t.steps)?t.steps.length:0}),n.workflows=e}const p=l||n.workflows.find(e=>e.workflowId===o);if(p){M("workflow_execute");try{const t=e.randomUUID(),r=await V({workflow:p,lineText:p.triggerCommand||p.name||"manual execute",lineSession:n,lineUserId:"",runId:t});a("[workflow] manual execute completed",{workflowId:o,workflowName:p.name||"",answer:r.answer||"",imageUrl:r.imageUrl||""})}catch(e){a("[workflow] manual execute failed",{workflowId:o,workflowName:p.name||"",error:e instanceof Error?e.message:String(e)})}finally{F("workflow_execute_done")}}else a("[workflow] execute ignored, workflow not found in runtime",{workflowId:o})},handleRunTask:async e=>{if(!s())throw new Error("bot session not ready");const o=String(e?.task_id||"").trim();if(!o)throw new Error("task_id is required");const n=String(e?.idempotency_key||"").trim(),l=W.get(o);if(l&&"completed"===l.status)return a("[task] duplicate run_task ignored",{taskId:o,idempotencyKey:n}),i(o,"task_completed",{duplicate:!0,idempotency_key:n||void 0,result:l.result||"",ts:(new Date).toISOString()}),l;const p=e?.constraints&&"object"==typeof e.constraints?t(e.constraints):{},d=Math.max(0,Math.min(Number(p.max_retries||0),5)),c=Math.max(10,Math.min(Number(p.timeout_sec||120),1800)),u=String(e?.goal||"").trim(),f=(Array.isArray(e?.steps)?e.steps:[]).map((e,r)=>((e,r)=>{const s=t(e),o=String(s.action||"").trim(),n=s.params&&"object"==typeof s.params?s.params:{},a=Array.isArray(s.actions)?s.actions:[],i=[];if(o)i.push({type:o,params:n});else for(const e of a){const r=t(e),s=String(r.type||"").trim(),o=r.params&&"object"==typeof r.params?r.params:{};s&&i.push({type:s,params:o})}return{stepId:String(s.step_id||s.id||`s${r+1}`).trim()||`s${r+1}`,title:String(s.title||s.name||`Step ${r+1}`).trim()||`Step ${r+1}`,instruction:String(s.instruction||"").trim(),verify:s.verify&&"object"==typeof s.verify?s.verify:null,actions:i}})(e,r));if(!f.length)throw new Error("run_task requires non-empty steps");M("run_task"),i(o,"task_received",{task_id:o,idempotency_key:n||void 0,step_total:f.length,max_retries:d,timeout_sec:c,ts:(new Date).toISOString()});try{const e=async()=>{const e=w(u||f.map(e=>e.instruction).join("\n"))||"zh",t=[],s=[];for(let n=0;n<f.length;n+=1){const a=f[n];let l=0,p=!1;for(;l<=d&&!p;){l+=1,i(o,"step_started",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,actions:a.actions,ts:(new Date).toISOString()});let w=Array.isArray(a.actions)?[...a.actions]:[];if(!w.length&&a.instruction){const t=await b(a.instruction,e,{forceAction:!0});w=r(t?.actions||[])}if(!w.length){const e="step has no executable actions";if(i(o,"step_failed",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,error_code:"BOT_STEP_NO_ACTIONS",error_message:e,retryable:l<=d,ts:(new Date).toISOString()}),l<=d){i(o,"step_retry",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,max_retries:d,reason:e,ts:(new Date).toISOString()});continue}throw new Error(e)}const c=await L(w,{runId:o,workflowId:"",workflowName:`run_task:${o}`,stepTitle:a.title,stepIndex:n+1,stepTotal:f.length});t.push(...Array.isArray(c.logs)?c.logs:[]),s.push(...Array.isArray(c.artifacts)?c.artifacts:[]);for(const e of c.artifacts||[])i(o,"artifact_created",{step_id:a.stepId,step_index:n+1,type:String(e?.type||""),file_url:String(e?.fileUrl||""),local_path:String(e?.localPath||""),ts:(new Date).toISOString()});const u=(c.logs||[]).find(e=>!e?.ok),g=K({verify:a.verify,execution:c});if(!u&&g.ok){i(o,"step_verified",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,verify:g,ts:(new Date).toISOString()}),p=!0;continue}const m=u?`action failed: ${String(u.type||"unknown")}`:`verify failed: ${g.reason}`,k=l<=d;if(i(o,"step_failed",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,verify:g,error_code:u?"BOT_ACTION_FAILED":"BOT_VERIFY_FAILED",error_message:m,retryable:k,ts:(new Date).toISOString()}),!k)throw new Error(m);i(o,"step_retry",{step_id:a.stepId,step_title:a.title,step_index:n+1,step_total:f.length,attempt:l,max_retries:d,reason:m,ts:(new Date).toISOString()})}}const a=await R({userText:u||`run_task ${o}`,language:e,execution:{logs:t,artifacts:s},previousResponseId:""}).catch(()=>({text:""}));return i(o,"ai_output",{text:String(a?.text||"").trim(),ts:(new Date).toISOString()}),i(o,"task_completed",{task_id:o,idempotency_key:n||void 0,summary:String(a?.text||"").trim(),step_total:f.length,ts:(new Date).toISOString()}),W.set(o,{status:"completed",result:String(a?.text||"").trim(),completedAt:Date.now()}),{ok:!0,summary:String(a?.text||"").trim()}};return await(async(e,t,r="run_task timeout")=>!Number.isFinite(t)||t<=0?e:Promise.race([e,new Promise((e,s)=>{setTimeout(()=>s(new Error(r)),t)})]))(e(),1e3*c,`run_task timeout after ${c}s`)}catch(e){const t=e instanceof Error?e.message:String(e);throw i(o,"task_failed",{task_id:o,idempotency_key:n||void 0,error_code:/timeout/i.test(t)?"BOT_TIMEOUT":"BOT_TASK_FAILED",error_message:t,retryable:!1,ts:(new Date).toISOString()}),W.set(o,{status:"failed",result:t,completedAt:Date.now()}),e}finally{F("run_task_done")}},getWorkflowCount:async()=>{const e=s();if(!e?.userId||!e?.botId)return 0;const t=await $().catch(()=>[]);return Array.isArray(t)?t.length:0}}};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import t from"node:fs/promises";import e from"node:path";const r=t=>{const e=String(t||"");return e?e.length<=8?"****":`${e.slice(0,3)}****${e.slice(-3)}`:""},n=t=>/(password|token|authorization|api_?key|secret|cookie|jwt)/i.test(String(t||"")),o=(t,e="",i=!0)=>{if(!i)return t;if(null==t)return t;if("string"==typeof t)return n(e)?r(t):/^Bearer\s+/i.test(t)?`Bearer ${r(t.slice(7))}`:t;if("number"==typeof t||"boolean"==typeof t)return t;if(Array.isArray(t))return t.map(t=>o(t,e,i));if("object"==typeof t){const e={};for(const[a,s]of Object.entries(t))n(a)?e[a]="string"==typeof s?r(s):"****":e[a]=o(s,a,i);return e}return String(t)},i=t=>{const e=t&&"object"==typeof t?t:null;return e&&"string"==typeof e.code?e.code:""};export const createDebugLogger=({env:r,getRuntimeContext:n})=>{let a=Promise.resolve(),s=!1;const u=n=>{const o=r?.debugLogPath;o&&(a=a.then(async()=>{await(async()=>{if(!r?.debugLogPath)return;if(s)return;const n=e.dirname(r.debugLogPath);await t.mkdir(n,{recursive:!0}),s=!0})(),await(async()=>{if(!r?.debugLogPath)return;const e=1024*Math.max(1,Number(r.debugLogMaxMb||50))*1024;try{if((await t.stat(r.debugLogPath)).size<e)return}catch(t){if("ENOENT"===i(t))return;throw t}const n=`${r.debugLogPath}.1`;try{await t.unlink(n)}catch(t){if("ENOENT"!==i(t))throw t}await t.rename(r.debugLogPath,n)})(),await t.appendFile(o,n,"utf8")}).catch(()=>{}))};return{emitDebugEvent:t=>{if(!r?.debugLogPath)return;const e=String(t?.level||"info").trim().toLowerCase();if("info"===r.debugLogLevel&&"debug"===e)return;const i="function"==typeof n?n():{},a={ts:(new Date).toISOString(),level:e,phase:String(t?.phase||"runtime"),event:String(t?.event||"log"),run_id:String(t?.runId||""),workflow_id:String(t?.workflowId||""),workflow_name:String(t?.workflowName||""),step_index:Number(t?.stepIndex||0),step_total:Number(t?.stepTotal||0),step_title:String(t?.stepTitle||""),bot_id:String(i.botId||""),user_id:String(i.userId||""),data:o(t?.data||{},"",Boolean(r.debugLogRedact))};u(`${JSON.stringify(a)}\n`)}}};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{chromium as t}from"playwright";export const createTaskExecutorService=({env:e,getHeadless:o,emitLog:n,emitDebugEvent:a,notifyBrowserUnavailable:r,normalizeActionUrl:i,resolveHostFromUrl:s,isValidOpenUrl:l,captureScreenshotAndUpload:p,writeArtifactFile:u})=>{let w=null,c=null,d=null,m=null,f="",h="";const g=new Map,k=new Map;let _=!1;const y=async()=>{if(d&&!d.isClosed())return d;if(!w){const a=()=>t.launch({headless:o()}),i=e.playwrightChannel||("win32"===process.platform?"msedge":"chrome"),s=()=>t.launch({headless:o(),channel:i});try{w=await a(),n("[playwright] browser launched",{strategy:"bundled-chromium"})}catch(t){const e=t instanceof Error?t.message:String(t);if(!(e.includes("Executable doesn't exist")||e.includes("playwright install")))throw t;n("[playwright] bundled chromium missing, fallback to system channel",{channel:i});try{w=await s(),n("[playwright] browser launched",{strategy:"system-channel",channel:i})}catch(t){const e=t instanceof Error?t.message:String(t),o=`No usable browser found. Chromium is missing and system channel "${i}" failed: ${e}`;throw n("[playwright] no usable browser found",{channel:i,error:e}),_||(_=!0,r?.({channel:i,reason:e,message:o})),new Error(o)}}}return c||(c=await w.newContext(m?{viewport:{width:1366,height:900},httpCredentials:m}:{viewport:{width:1366,height:900}})),d=await c.newPage(),d.setDefaultNavigationTimeout(e.playwrightNavigationTimeoutMs),d},I=async(t=null)=>{await y();try{d&&!d.isClosed()&&await d.close()}catch{}finally{d=null}try{c&&await c.close()}catch{}finally{c=null}if(!w)throw new Error("browser not initialized");return c=await w.newContext(t?{viewport:{width:1366,height:900},httpCredentials:t}:{viewport:{width:1366,height:900}}),d=await c.newPage(),d.setDefaultNavigationTimeout(e.playwrightNavigationTimeoutMs),d},b=async({username:t,password:e,host:o,url:n})=>{const a=String(t||"").trim(),r=String(e||"").trim();if(!a||!r)throw new Error("set_http_credentials requires username and password");m={username:a,password:r};const i=String(o||"").trim().toLowerCase()||s(n||"")||s(h)||s(f);if(i){k.set(i,{username:a,password:r});const t=Buffer.from(`${a}:${r}`,"utf8").toString("base64");g.set(i,`Basic ${t}`)}},T=async(t,e,o)=>{for(const n of e)try{const e=t.locator(n).first();if(!await e.count())continue;return await e.fill(String(o||"")),n}catch{}return""},N=async(t,e,o,n=1e4)=>{const a=Date.now()+Math.max(500,Number(n||1e4));for(;Date.now()<a;){const n=await T(t,e,o);if(n)return n;await t.waitForTimeout(250)}return""},x=async(t,e)=>{for(const o of e)try{const e=t.locator(o).first();if(!await e.count())continue;return await e.click(),o}catch{}return""},v=async t=>{const o=Math.max(0,Math.min(Number(e.playwrightJsSettleMaxMs||3e3),1e4));if(!o)return{waited:!1,maxMs:o,state:"skipped"};const n=Date.now();let a="timeout";try{await Promise.all([t.waitForLoadState("networkidle",{timeout:o}),t.waitForFunction(()=>"complete"===document.readyState,void 0,{timeout:o})]),a="settled"}catch{a="timeout"}return{waited:!0,maxMs:o,state:a,elapsedMs:Date.now()-n}};return{executeActionPlan:async(t,e={})=>{const o=[],r=[];let w=await y();for(let c=0;c<t.length;c+=1){const d=t[c]||{},m=String(d.type||"").trim(),_=d.params&&"object"==typeof d.params?d.params:{};a({level:"debug",phase:"action_start",event:"action_started",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m||"unknown",params:_}});try{if("open_url"===m){const t=String(_.url||"").trim();if(!t)throw new Error("open_url missing url");const n=i(t);if(!l(n))throw new Error(`invalid open_url host: ${n}`);f=n;const r=s(n),p=r&&g.get(r)||"",u=r&&k.get(r)||null;p?await w.setExtraHTTPHeaders({Authorization:p}):await w.setExtraHTTPHeaders({});try{await w.goto(n,{waitUntil:"domcontentloaded"})}catch(t){const e=t instanceof Error?t.message:String(t);if(!(Boolean(u)&&(/ERR_INVALID_AUTH_CREDENTIALS/i.test(e)||/\b401\b/.test(e))))throw t;w=await I(u),await w.goto(n,{waitUntil:"domcontentloaded"})}const d=await v(w);try{h=String(w.url()||"").trim()||n}catch{h=n}o.push({step:c+1,type:m,ok:!0,url:n,rawUrl:t,js_settle:d}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,url:n,js_settle:d}});continue}if("set_http_credentials"===m){const t=String(_.username||"").trim(),n=String(_.password||"").trim();if(!t||!n)throw new Error("set_http_credentials missing username/password");await b({username:t,password:n,host:String(_.host||""),url:String(_.url||"")}),o.push({step:c+1,type:m,ok:!0,username:t,host:String(_.host||"")}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,host:String(_.host||"")}});continue}if("auto_login_form"===m){const t=String(_.username||"").trim(),n=String(_.password||"").trim();if(!t||!n)throw new Error("auto_login_form missing username/password");const r=await N(w,['input[name*="user" i]','input[name*="login" i]','input[name*="id" i]','input[autocomplete="username"]','input[id*="user" i]','input[id*="login" i]','input[id*="id" i]','input[type="text"]','input[type="email"]',"input:not([type])"],t),i=await N(w,['input[type="password"]','input[autocomplete="current-password"]','input[name*="pass" i]','input[id*="pass" i]'],n);if(!r||!i){if(!await w.locator('input[type="password"]').count().catch(()=>0)){o.push({step:c+1,type:m,ok:!0,skipped:!0,reason:"login_form_not_present"}),a({level:"debug",phase:"action_end",event:"action_skipped",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,skipped:!0,reason:"login_form_not_present"}});continue}}if(!r)throw new Error("auto_login_form cannot find username input");if(!i)throw new Error("auto_login_form cannot find password input");const s=await x(w,['button[type="submit"]','input[type="submit"]','button[name*="login" i]','button[id*="login" i]',"button"]);s||await w.keyboard.press("Enter"),await w.waitForTimeout(1200),o.push({step:c+1,type:m,ok:!0,user_selector:r,pass_selector:i,submit_selector:s||"keyboard:Enter"}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,user_selector:r,pass_selector:i,submit_selector:s||"keyboard:Enter"}});continue}if("wait"===m){const t=Math.max(0,Math.min(Number(_.ms||1e3),15e3));await w.waitForTimeout(t),o.push({step:c+1,type:m,ok:!0,ms:t}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,ms:t}});continue}if("screenshot"===m){const t=await p(w,String(_.filename||""));r.push({type:"image",...t}),o.push({step:c+1,type:m,ok:!0,fileUrl:t.fileUrl}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,file_url:t.fileUrl||""}});continue}if("read_page_text"===m){const t=Math.max(200,Math.min(Number(_.max_chars||2e3),1e4)),n=await w.evaluate(t=>{const e=document.body?document.body.innerText:"";return String(e||"").slice(0,Number(t||2e3))},t);o.push({step:c+1,type:m,ok:!0,text:String(n||"")}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,text_chars:String(n||"").length}});continue}if("http_get"===m){const t=String(_.url||"").trim();if(!t)throw new Error("http_get missing url");const n=await fetch(t,{method:"GET"}),r=await n.text();o.push({step:c+1,type:m,ok:n.ok,status:n.status,bodyPreview:String(r||"").slice(0,2e3)}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:n.ok,status:n.status}});continue}if("write_file"===m){const t=await u({filename:String(_.filename||""),content:String(_.content||"")});r.push({type:"file",localPath:t.localPath}),o.push({step:c+1,type:m,ok:!0,localPath:t.localPath}),a({level:"debug",phase:"action_end",event:"action_completed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m,ok:!0,local_path:t.localPath}});continue}o.push({step:c+1,type:m||"unknown",ok:!1,error:"unsupported action"})}catch(t){const r=t instanceof Error?t.message:String(t);o.push({step:c+1,type:m||"unknown",ok:!1,error:r}),a({level:"info",phase:"action_error",event:"action_failed",runId:e.runId||"",workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepIndex:Number(e.stepIndex||0),stepTotal:Number(e.stepTotal||0),stepTitle:e.stepTitle||"",data:{action_index:c+1,action_type:m||"unknown",error:r,params:_}}),n("[task] action failed",{step:c+1,type:m||"unknown",error:r,params:_,workflowId:e.workflowId||"",workflowName:e.workflowName||"",stepTitle:e.stepTitle||"",workflowStepIndex:Number(e.stepIndex||0),workflowStepTotal:Number(e.stepTotal||0)})}}const c=r.find(t=>"image"===t.type);return{logs:o,artifacts:r,firstImageUrl:c?.fileUrl||"",firstImagePreviewUrl:c?.fileUrl||""}},closeTaskBrowser:async()=>{try{c&&await c.close()}catch{}finally{c=null,d=null}try{w&&await w.close()}catch{}finally{w=null}},resetWorkflowState:()=>{f="",h="",g.clear(),k.clear()},resolveTaskFallbackUrl:()=>{const t=String(h||"").trim();if(t&&l(t))return t;const e=String(f||"").trim();if(e&&l(e))return e;try{const t=String(d?.url?.()||"").trim();if(t&&l(t))return t}catch{}return""}}};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const safeJsonParse=t=>{try{return JSON.parse(t)}catch{return null}};export const parseJsonLoose=t=>{const r=String(t||"").trim();if(!r)return null;const s=safeJsonParse(r);if(s&&"object"==typeof s)return s;const n=r.indexOf("{");if(n<0)return null;let e=0,o=!1,i=!1;for(let t=n;t<r.length;t+=1){const s=r[t];if(o)i?i=!1:"\\"===s?i=!0:'"'===s&&(o=!1);else if('"'!==s){if("{"===s&&(e+=1),"}"===s&&(e-=1,0===e)){const s=r.slice(n,t+1),e=safeJsonParse(s);if(e&&"object"==typeof e)return e;break}}else o=!0}return null};export const normalizeActionUrl=t=>{const r=String(t||"").trim();return r?/^https?:\/\//i.test(r)?r:/^[a-z0-9.-]+\.[a-z]{2,}(\/.*)?$/i.test(r)?`https://${r}`:r:""};export const resolveHostFromUrl=t=>{try{const r=new URL(String(t||"").trim());return String(r.host||"").trim().toLowerCase()}catch{return""}};export const isValidOpenUrl=t=>{try{const r=new URL(String(t||"").trim());if(!/^https?:$/i.test(r.protocol))return!1;const s=String(r.hostname||"").trim();if(!s)return!1;const n="localhost"===s.toLowerCase(),e=/^(\d{1,3}\.){3}\d{1,3}$/.test(s),o=s.includes(".");return n||e||o}catch{return!1}};export const extractFirstUrl=t=>{const r=String(t||"");if(!r)return"";const s=r.match(/https?:\/\/[^\s"'<>`]+/i)||r.match(/[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s"'<>`]*)?/i);return normalizeActionUrl(s?.[0]||"")};export const extractBasicCredentials=t=>{const r=String(t||"");if(!r)return null;const s=r.match(/(?:用户名|使用者|ユーザー名|user(?:name)?|login)\s*[::]?\s*([^\s\n]+)/i)||r.match(/(?:id)\s*[::]?\s*([^\s\n]+)/i),n=r.match(/(?:密码|パスワード|password|pass)\s*[::]?\s*([^\s\n]+)/i),e=String(s?.[1]||"").trim(),o=String(n?.[1]||"").trim();if(!e||!o)return null;const i=extractFirstUrl(r);let c="";if(i)try{c=new URL(i).host}catch{c=""}return{username:e,password:o,url:i,host:c}};export const isBasicAuthStep=(t,r)=>{const s=`${String(t||"")}\n${String(r||"")}`.toLowerCase();return/(basic|ベーシック|基本认证|basic认证|basic 認証)/i.test(s)};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const detectLanguage=t=>{const e=String(t||"");return/[\u3040-\u30ff]/.test(e)?"ja":/[\u4e00-\u9fff]/.test(e)?"zh":"en"};const t=t=>{const e=String(t||"");return(e.match(/[\u4e00-\u9fff\u3040-\u30ffA-Za-z0-9]/g)||[]).length-2*(e.match(/[\uFF61-\uFF9F\uFFFD]/g)||[]).length};export const normalizeIncomingText=e=>{const n=String(e||"");if(!n)return"";const r=Buffer.from(n,"latin1").toString("utf8");return r&&r!==n&&t(r)>t(n)?r:n};export const isLikelyMojibake=t=>{const e=String(t||"");if(!e)return!1;const n=/[。「」、・ヲ-゚]/.test(e),r=/[\u4e00-\u9fff]/.test(e),o=(e.match(/\uFFFD/g)||[]).length,i=(e.match(/[。「」、・]/g)||[]).length;return o>0||(!(!n||!r)||i>=3)};export const buildLanguageInstruction=t=>"ja"===t?"Respond in Japanese.":"zh"===t?"Respond in Chinese.":"Respond in English.";export const buildEmptyReply=t=>"ja"===t?"応答が空でした。もう一度短く送ってください。":"en"===t?"I returned an empty answer. Please send your request again briefly.":"这次回复为空,请再简短发一次。";export const buildActionFailedReply=t=>"ja"===t?"タスク実行に失敗しました。短く言い換えてもう一度送ってください。":"en"===t?"Task execution failed. Please retry with a shorter, clearer request.":"任务执行失败,请换一种更简短明确的说法重试。";export const buildMojibakeReply=t=>"ja"===t?"メッセージが文字化けして読めません。短く打ち直して再送してください。":"en"===t?"Your message looks garbled. Please resend it in plain text.":"消息疑似乱码,请用纯文本重发一次。";export const buildConfirmPrompt=(t,e)=>{const n=e?.name||e?.triggerCommand||"workflow";return"ja"===t?`指令「${n}」を実行しますか?「确认」または「取消」で返信してください。`:"en"===t?`Run command "${n}" now? Reply "confirm" or "cancel".`:`检测到指令「${n}」,是否执行?回复“确认”或“取消”。`};export const buildCancelReply=t=>"ja"===t?"已取消本次执行。":"en"===t?"Execution cancelled.":"已取消本次执行。";export const buildStepStartReply=(t,e,n,r)=>"ja"===t?`步骤 ${e}/${n} 开始:${r}`:"en"===t?`Step ${e}/${n} started: ${r}`:`步骤 ${e}/${n} 开始:${r}`;export const buildStepFallbackSummary=(t,e,n,r)=>"ja"===t?`步骤 ${e}/${n} 完成:${r}`:"en"===t?`Step ${e}/${n} done: ${r}`:`步骤 ${e}/${n} 完成:${r}`;export const isNumericChoice=t=>/^\d+$/.test(String(t||"").trim());export const isConfirmText=t=>/^(确认|確認|ok|yes|y|执行|開始|start|go)$/i.test(String(t||"").trim());export const isCancelText=t=>/^(取消|キャンセル|cancel|no|n|stop)$/i.test(String(t||"").trim());export const isLikelyNewTask=t=>{const e=String(t||"").trim().toLowerCase();return!!e&&/(帮我|请帮|查一下|调查|搜索|打开|下载|find|search|open|check|look up)/i.test(e)};const e=t=>String(t||"").toLowerCase().replace(/[`~!@#$%^&*()_+=[\]{};:'",.<>/?\\|-]/g," ").replace(/\s+/g," ").trim(),n=t=>{const n=e(t),r=new Set(["http","https","www","com","net","org","co","jp","cn"]);return Array.from(new Set((n.match(/[a-z0-9]{3,}/g)||[]).map(t=>t.trim()).filter(t=>t&&!r.has(t))))},r=t=>{const e=String(t||"").trim();return Array.from(new Set((e.match(/[\u4e00-\u9fff]{2,}/g)||[]).map(t=>t.trim()).filter(Boolean)))},o=t=>{const n=(Array.isArray(t?.steps)?t.steps:[]).map(t=>`${t?.title||""} ${t?.instruction||""}`.trim()).join(" ");return e([t?.triggerCommand||"",t?.name||"",t?.description||"",n].join(" "))},i=(t,e)=>{if(!t||t.length<2||!e)return!1;if(e.includes(t))return!0;return(e.match(/[a-z0-9]{2,}/g)||[]).some(e=>e.startsWith(t)||t.startsWith(e))};export const matchWorkflowByCommand=(t,s)=>{const c=String(t||"").trim(),a=e(c);if(!a)return null;let u=null,l=0;const m=/https?:\/\/\S+/i.test(c)||/[a-z0-9.-]+\.[a-z]{2,}(?:\/\S*)?/i.test(c);for(const t of s||[]){const s=e(t?.triggerCommand||"");if(!s)continue;if(a===s)return t;if(a.startsWith(s)||a.includes(s))return t;if(m)continue;const p=o(t);if(!p)continue;let f=0;p.includes(a)&&(f+=.9),a.includes(s)&&(f+=.8);const g=n(c);for(const t of g)i(t,p)&&(f+=.35);const d=r(c);for(const t of d)t.length>=2&&p.includes(t)&&(f+=.4);f>l&&(l=f,u=t)}return l>=.7?u:null};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import t from"node:crypto";export const normalizeWorkflow=i=>({workflowId:String(i?.workflow_id||""),name:String(i?.name||"").trim(),triggerCommand:String(i?.trigger_command||"").trim(),description:String(i?.description||"").trim(),botId:i?.bot_id?String(i.bot_id).trim():"",isActive:Boolean(i?.is_active),steps:(Array.isArray(i?.steps_json)?i.steps_json:[]).map((i,r)=>((i,r)=>({id:String(i?.id||t.randomUUID()),title:String(i?.title||`Step ${r+1}`).trim()||`Step ${r+1}`,instruction:String(i?.instruction||"").trim()}))(i,r)).filter(t=>t.instruction)});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
|
-
const { spawnSync } = require('node:child_process');
|
|
5
|
-
|
|
6
|
-
const getArg = (name, fallback = '') => {
|
|
7
|
-
const key = `--${name}`;
|
|
8
|
-
const idx = process.argv.findIndex((x) => x === key);
|
|
9
|
-
if (idx >= 0 && idx + 1 < process.argv.length) return String(process.argv[idx + 1] || '');
|
|
10
|
-
const pair = process.argv.find((x) => x.startsWith(`${key}=`));
|
|
11
|
-
if (pair) return String(pair.slice(key.length + 1));
|
|
12
|
-
return fallback;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const botRoot = process.cwd();
|
|
16
|
-
const email = getArg('email', process.env.BOT_SCRIPT_EMAIL || '');
|
|
17
|
-
const password = getArg('password', process.env.BOT_SCRIPT_PASSWORD || '');
|
|
18
|
-
const botId = getArg('bot-id', process.env.BOT_SCRIPT_BOT_ID || '');
|
|
19
|
-
const workflowFile = getArg('workflow-file', path.join('scripts', 'workflows', 'kaitori-login.json'));
|
|
20
|
-
const lineText = getArg('line-text', '执行该流程');
|
|
21
|
-
const lang = getArg('lang', 'zh');
|
|
22
|
-
const iterations = Math.max(1, Number(getArg('iterations', '2')));
|
|
23
|
-
const debugLogPath = getArg('debug-log-path', process.env.BOT_DEBUG_LOG_PATH || path.join(botRoot, 'tmp', 'bot-debug.jsonl'));
|
|
24
|
-
|
|
25
|
-
if (!email || !password || !botId) {
|
|
26
|
-
process.stderr.write('Missing required args: --email --password --bot-id\n');
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const parseJsonSafe = (raw) => {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(raw);
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const loadRunEvents = (filePath, runId) => {
|
|
39
|
-
if (!fs.existsSync(filePath)) return [];
|
|
40
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
41
|
-
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
42
|
-
const events = [];
|
|
43
|
-
for (const line of lines) {
|
|
44
|
-
const json = parseJsonSafe(line);
|
|
45
|
-
if (!json || json.run_id !== runId) continue;
|
|
46
|
-
events.push(json);
|
|
47
|
-
}
|
|
48
|
-
return events;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const suggestImprovements = (events, stderrMessage = '') => {
|
|
52
|
-
const fails = events.filter((x) => x.event === 'action_failed');
|
|
53
|
-
const suggestions = [];
|
|
54
|
-
for (const f of fails) {
|
|
55
|
-
const msg = String(f?.data?.error || '');
|
|
56
|
-
if (msg.includes('auto_login_form cannot find username input')) {
|
|
57
|
-
suggestions.push('admin登录页输入框未识别:建议在step中追加“账号输入框selector”和“密码输入框selector”提示。');
|
|
58
|
-
} else if (msg.includes('ERR_NAME_NOT_RESOLVED')) {
|
|
59
|
-
suggestions.push('URL无法解析:请确认域名拼写,优先使用完整 https:// URL。');
|
|
60
|
-
} else if (msg.includes('set_http_credentials')) {
|
|
61
|
-
suggestions.push('Basic认证参数不完整:请在step中明确“用户名/密码”,并先执行URL步骤。');
|
|
62
|
-
} else {
|
|
63
|
-
suggestions.push(`失败步骤 ${f.step_index}/${f.step_total}:${msg || 'unknown error'}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const err = String(stderrMessage || '');
|
|
67
|
-
if (err.includes('ERR_INVALID_AUTH_CREDENTIALS')) {
|
|
68
|
-
suggestions.push('Basic认证失败:把“输入basic认证信息”放到打开URL之前,或在同一步里同时给出URL+用户名+密码。');
|
|
69
|
-
}
|
|
70
|
-
return Array.from(new Set(suggestions));
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const runOnce = (runId) => {
|
|
74
|
-
const nodeCmd = process.execPath;
|
|
75
|
-
const scriptPath = path.join(botRoot, 'scripts', 'run-workflow.cjs');
|
|
76
|
-
const args = [
|
|
77
|
-
scriptPath,
|
|
78
|
-
'--email',
|
|
79
|
-
email,
|
|
80
|
-
'--password',
|
|
81
|
-
password,
|
|
82
|
-
'--bot-id',
|
|
83
|
-
botId,
|
|
84
|
-
'--workflow-file',
|
|
85
|
-
workflowFile,
|
|
86
|
-
'--line-text',
|
|
87
|
-
lineText,
|
|
88
|
-
'--lang',
|
|
89
|
-
lang,
|
|
90
|
-
'--run-id',
|
|
91
|
-
runId,
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
const env = {
|
|
95
|
-
...process.env,
|
|
96
|
-
BOT_DEBUG_LOG_PATH: debugLogPath,
|
|
97
|
-
BOT_DEBUG_LOG_LEVEL: process.env.BOT_DEBUG_LOG_LEVEL || 'debug',
|
|
98
|
-
BOT_DEBUG_LOG_REDACT: process.env.BOT_DEBUG_LOG_REDACT || 'true',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const result = spawnSync(nodeCmd, args, {
|
|
102
|
-
cwd: botRoot,
|
|
103
|
-
env,
|
|
104
|
-
encoding: 'utf8',
|
|
105
|
-
});
|
|
106
|
-
return result;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const all = [];
|
|
110
|
-
for (let i = 0; i < iterations; i += 1) {
|
|
111
|
-
const runId = crypto.randomUUID();
|
|
112
|
-
process.stdout.write(`\n[backtest] iteration ${i + 1}/${iterations}, run_id=${runId}\n`);
|
|
113
|
-
const execResult = runOnce(runId);
|
|
114
|
-
const outJson = parseJsonSafe(execResult.stdout || '');
|
|
115
|
-
const errJson = parseJsonSafe(execResult.stderr || '');
|
|
116
|
-
const events = loadRunEvents(debugLogPath, runId);
|
|
117
|
-
const failedActions = events.filter((x) => x.event === 'action_failed');
|
|
118
|
-
const summary = {
|
|
119
|
-
run_id: runId,
|
|
120
|
-
exit_code: Number(execResult.status || 0),
|
|
121
|
-
ok: Boolean(outJson?.ok),
|
|
122
|
-
answer: String(outJson?.answer || ''),
|
|
123
|
-
image_url: String(outJson?.image_url || ''),
|
|
124
|
-
events: events.length,
|
|
125
|
-
failed_actions: failedActions.length,
|
|
126
|
-
suggestions: suggestImprovements(events, errJson?.error || execResult.stderr || ''),
|
|
127
|
-
stderr: errJson || (execResult.stderr ? execResult.stderr.trim() : ''),
|
|
128
|
-
stdout_raw: outJson ? '' : (execResult.stdout || '').trim(),
|
|
129
|
-
};
|
|
130
|
-
all.push(summary);
|
|
131
|
-
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const reportPath = path.join(botRoot, 'tmp', `backtest-report-${Date.now()}.json`);
|
|
135
|
-
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
136
|
-
fs.writeFileSync(reportPath, JSON.stringify({ generated_at: new Date().toISOString(), runs: all }, null, 2), 'utf8');
|
|
137
|
-
process.stdout.write(`\n[backtest] report: ${reportPath}\n`);
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const { spawn } = require('node:child_process');
|
|
5
|
-
|
|
6
|
-
const root = process.cwd();
|
|
7
|
-
const targetBrowsersPath = path.join(root, 'pw-browsers');
|
|
8
|
-
const isWindowsCi = process.platform === 'win32' && String(process.env.CI || '').toLowerCase() === 'true';
|
|
9
|
-
const tempBase = process.env.RUNNER_TEMP || process.env.TEMP || '';
|
|
10
|
-
const modeArg = process.argv.find((x) => String(x || '').startsWith('--mode=')) || '';
|
|
11
|
-
const installMode = String(modeArg.split('=')[1] || process.env.PLAYWRIGHT_INSTALL_MODE || 'shared')
|
|
12
|
-
.trim()
|
|
13
|
-
.toLowerCase();
|
|
14
|
-
const isBundleMode = installMode === 'bundle';
|
|
15
|
-
const resolveSharedBrowsersPath = () => {
|
|
16
|
-
if (process.platform === 'win32') {
|
|
17
|
-
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
18
|
-
return path.join(base, 'weget-bot', 'pw-browsers');
|
|
19
|
-
}
|
|
20
|
-
if (process.platform === 'darwin') {
|
|
21
|
-
return path.join(os.homedir(), 'Library', 'Caches', 'weget-bot', 'pw-browsers');
|
|
22
|
-
}
|
|
23
|
-
return path.join(os.homedir(), '.cache', 'weget-bot', 'pw-browsers');
|
|
24
|
-
};
|
|
25
|
-
const defaultInstallPath = isBundleMode ? targetBrowsersPath : resolveSharedBrowsersPath();
|
|
26
|
-
const installBrowsersPath =
|
|
27
|
-
process.env.PLAYWRIGHT_BROWSERS_PATH ||
|
|
28
|
-
(isWindowsCi && tempBase && isBundleMode ? path.join(tempBase, 'pw-browsers') : defaultInstallPath);
|
|
29
|
-
|
|
30
|
-
if (!fs.existsSync(installBrowsersPath)) {
|
|
31
|
-
fs.mkdirSync(installBrowsersPath, { recursive: true });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const env = {
|
|
35
|
-
...process.env,
|
|
36
|
-
PLAYWRIGHT_BROWSERS_PATH: installBrowsersPath,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const resolvePlaywrightInstallCommand = () => {
|
|
40
|
-
try {
|
|
41
|
-
const playwrightPkgPath = require.resolve('playwright/package.json', { paths: [root] });
|
|
42
|
-
const playwrightDir = path.dirname(playwrightPkgPath);
|
|
43
|
-
const cliPath = path.join(playwrightDir, 'cli.js');
|
|
44
|
-
if (fs.existsSync(cliPath)) {
|
|
45
|
-
return {
|
|
46
|
-
cmd: process.execPath,
|
|
47
|
-
args: [cliPath, 'install', 'chromium'],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
// Fallback to npx when local playwright package cannot be resolved.
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
cmd: process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
55
|
-
args: ['playwright', 'install', 'chromium'],
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const installCommand = resolvePlaywrightInstallCommand();
|
|
60
|
-
const maxAttempts = Number(process.env.PLAYWRIGHT_INSTALL_RETRIES || 3);
|
|
61
|
-
const defaultAllowFailure = process.platform === 'win32' || Boolean(process.env.CI);
|
|
62
|
-
const allowFailure =
|
|
63
|
-
String(process.env.PLAYWRIGHT_INSTALL_ALLOW_FAILURE || (defaultAllowFailure ? 'true' : 'false')).toLowerCase() ===
|
|
64
|
-
'true';
|
|
65
|
-
|
|
66
|
-
const runInstallAttempt = (attempt, total) =>
|
|
67
|
-
new Promise((resolve) => {
|
|
68
|
-
console.log(`[playwright] install attempt ${attempt}/${total}`);
|
|
69
|
-
console.log('[playwright] downloading browser binaries, this may take a while...');
|
|
70
|
-
console.log('[playwright] waiting for download progress output...');
|
|
71
|
-
const startedAt = Date.now();
|
|
72
|
-
const heartbeatTimer = setInterval(() => {
|
|
73
|
-
const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
|
|
74
|
-
console.log(`[playwright] still installing... elapsed ${elapsedSec}s (attempt ${attempt}/${total})`);
|
|
75
|
-
}, 8000);
|
|
76
|
-
const child = spawn(installCommand.cmd, installCommand.args, {
|
|
77
|
-
cwd: root,
|
|
78
|
-
stdio: 'inherit',
|
|
79
|
-
env,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
child.on('error', (error) => {
|
|
83
|
-
clearInterval(heartbeatTimer);
|
|
84
|
-
console.error(`[playwright] install process error: ${error.message}`);
|
|
85
|
-
resolve(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
child.on('close', (code) => {
|
|
89
|
-
clearInterval(heartbeatTimer);
|
|
90
|
-
const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
|
|
91
|
-
console.log(`[playwright] attempt ${attempt} finished in ${elapsedSec}s`);
|
|
92
|
-
resolve(typeof code === 'number' ? code : 1);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const hasInstalledChromium = (baseDir) => {
|
|
97
|
-
if (!fs.existsSync(baseDir)) return false;
|
|
98
|
-
const names = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
99
|
-
const chromiumDirs = names
|
|
100
|
-
.filter((x) => x.isDirectory() && String(x.name || '').startsWith('chromium-'))
|
|
101
|
-
.map((x) => x.name);
|
|
102
|
-
if (!chromiumDirs.length) return false;
|
|
103
|
-
|
|
104
|
-
for (const dirName of chromiumDirs) {
|
|
105
|
-
const rootDir = path.join(baseDir, dirName);
|
|
106
|
-
if (process.platform === 'win32') {
|
|
107
|
-
if (fs.existsSync(path.join(rootDir, 'chrome-win', 'chrome.exe'))) return true;
|
|
108
|
-
if (fs.existsSync(path.join(rootDir, 'chrome-win64', 'chrome.exe'))) return true;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (process.platform === 'darwin') {
|
|
112
|
-
const macCandidates = [
|
|
113
|
-
path.join(rootDir, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
|
|
114
|
-
path.join(rootDir, 'chrome-mac-arm64', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
|
|
115
|
-
];
|
|
116
|
-
if (macCandidates.some((candidate) => fs.existsSync(candidate))) return true;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (fs.existsSync(path.join(rootDir, 'chrome-linux', 'chrome'))) return true;
|
|
120
|
-
}
|
|
121
|
-
return false;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const run = async () => {
|
|
125
|
-
console.log(`[playwright] installing chromium to ${installBrowsersPath}`);
|
|
126
|
-
console.log(`[playwright] install mode: ${installMode}`);
|
|
127
|
-
console.log(`[playwright] using command: ${installCommand.cmd} ${installCommand.args.join(' ')}`);
|
|
128
|
-
if (process.platform === 'win32' && allowFailure) {
|
|
129
|
-
console.log('[playwright] windows mode: browser install failure will not block npm install');
|
|
130
|
-
}
|
|
131
|
-
if (hasInstalledChromium(installBrowsersPath)) {
|
|
132
|
-
console.log('[playwright] chromium already exists, skip download');
|
|
133
|
-
if (isBundleMode && path.resolve(installBrowsersPath) !== path.resolve(targetBrowsersPath)) {
|
|
134
|
-
if (fs.existsSync(targetBrowsersPath)) {
|
|
135
|
-
fs.rmSync(targetBrowsersPath, { recursive: true, force: true });
|
|
136
|
-
}
|
|
137
|
-
fs.mkdirSync(path.dirname(targetBrowsersPath), { recursive: true });
|
|
138
|
-
fs.cpSync(installBrowsersPath, targetBrowsersPath, { recursive: true });
|
|
139
|
-
console.log(`[playwright] copied browsers to ${targetBrowsersPath}`);
|
|
140
|
-
}
|
|
141
|
-
console.log('[playwright] chromium install completed');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
let status = 1;
|
|
145
|
-
for (let i = 1; i <= Math.max(1, maxAttempts); i += 1) {
|
|
146
|
-
// eslint-disable-next-line no-await-in-loop
|
|
147
|
-
status = await runInstallAttempt(i, Math.max(1, maxAttempts));
|
|
148
|
-
if (status === 0) {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
if (i < Math.max(1, maxAttempts)) {
|
|
152
|
-
console.warn('[playwright] install failed, retrying...');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (status !== 0) {
|
|
157
|
-
if (allowFailure) {
|
|
158
|
-
console.warn(
|
|
159
|
-
'[playwright] chromium install failed, continue build without bundled chromium (runtime will fallback to system browser channel).',
|
|
160
|
-
);
|
|
161
|
-
process.exit(0);
|
|
162
|
-
}
|
|
163
|
-
process.exit(status);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (isBundleMode && path.resolve(installBrowsersPath) !== path.resolve(targetBrowsersPath)) {
|
|
167
|
-
if (fs.existsSync(targetBrowsersPath)) {
|
|
168
|
-
fs.rmSync(targetBrowsersPath, { recursive: true, force: true });
|
|
169
|
-
}
|
|
170
|
-
fs.mkdirSync(path.dirname(targetBrowsersPath), { recursive: true });
|
|
171
|
-
fs.cpSync(installBrowsersPath, targetBrowsersPath, { recursive: true });
|
|
172
|
-
console.log(`[playwright] copied browsers to ${targetBrowsersPath}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
console.log('[playwright] chromium install completed');
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
run().catch((error) => {
|
|
179
|
-
console.error(`[playwright] unexpected error: ${error?.message || String(error)}`);
|
|
180
|
-
process.exit(1);
|
|
181
|
-
});
|