"""Create reproducible, importable Dify canvases from the application's prompts.""" import json import argparse import importlib.util from pathlib import Path parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--provider-id', help='Native model tool provider ID created by dify-model-tools.py') args = parser.parse_args() ROOT = Path(__file__).resolve().parents[1] DEST = ROOT / 'deploy/dify/workflows' DEST.mkdir(parents=True, exist_ok=True) events = (ROOT / 'internal/extraction/events.go').read_text(encoding='utf-8') memoir = (ROOT / 'internal/memoir/generation.go').read_text(encoding='utf-8') def constant(source, name): return source.split('const '+name+' = `', 1)[1].split('`', 1)[0] extract_prompt = constant(events, 'eventExtractionPrompt') daily_prompt = constant(events, 'dailyExtractionPrompt') book_prompt = constant(memoir, 'MemoirPrompt') def node(id, type, title, x, y, **data): return dict(id=id, type='custom', position=dict(x=x,y=y), positionAbsolute=dict(x=x,y=y), width=244, height=110, sourcePosition='right', targetPosition='left', data=dict(type=type,title=title,desc='',selected=False,**data)) def edge(a,b,nodes,iteration=None): types={n['id']:n['data']['type'] for n in nodes} data=dict(sourceType=types[a],targetType=types[b],isInIteration=bool(iteration),isInLoop=False) if iteration: data['iteration_id']=iteration return dict(id=a+'-'+b,source=a,target=b,sourceHandle='source',targetHandle='target',type='custom',zIndex=1002 if iteration else 0,data=data) def start(): return node('start','start','开始:接收后台任务',50,80,variables=[dict(variable=v,label=l,type='text-input',required=False,max_length=128,options=[],default='') for v,l in [('context_id','任务上下文(后台自动传入;留空可测试示例)'),('step_token','临时执行凭证(留空可测试示例)')]]) def http(id,title,step,x,y,kind=None,prompt=None,prepared=True,index=None): selector='prepared' if prepared else 'start' body={'context_id':'{{#'+selector+'.context_id#}}','step_token':'{{#'+selector+'.step_token#}}'} if kind:body['kind']=kind if prompt:body['prompt']=prompt text=json.dumps(body,ensure_ascii=False) if index is not None:text=text[:-1]+',"chapter_index":{{#pictures.item#}}}' return node(id,'http-request',title,x,y,method='POST',url='http://model-bridge:8188/steps/'+step,authorization={'type':'no-auth'},headers='Content-Type:application/json',params='',body={'type':'json','data':[{'id':'body','key':'','type':'text','value':text}]},timeout={'connect':15,'read':240,'write':30},retry_config={'enabled':False,'max_retries':1,'retry_interval':1000}) def parse(id,title,source,fields,x,y,allow_errors=False): code='import json\ndef main(body: str, status_code: int) -> dict:\n if status_code != 200: raise Exception("HTTP_STEP_FAILED_"+str(status_code))\n data=json.loads(body)\n' if not allow_errors:code+=' if not data.get("success"): raise Exception(data.get("error_code","MODEL_STEP_FAILED"))\n' code+=' return {'+','.join(repr(k)+': data.get('+repr(k)+','+('""' if typ=='string' else '[]' if typ.startswith('array') else '0')+')' for k,typ in fields.items())+'}\n' return node(id,'code',title,x,y,code_language='python3',code=code,variables=[{'variable':'body','value_selector':[source,'body']},{'variable':'status_code','value_selector':[source,'status_code']}],outputs={k:{'type':t,'children':None} for k,t in fields.items()}) def end(source,fields,x,y): return node('end','end','完成:由后台核验并保存',x,y,outputs=[dict(variable=k,value_type=t,value_selector=[source,k]) for k,t in fields.items()]) def app(name,description,nodes,edges): return dict(kind='app',version='0.7.0',dependencies=[],app=dict(name=name,description=description,mode='workflow',icon='📖',icon_background='#ECFDF3',use_icon_as_answer_icon=False),workflow=dict(conversation_variables=[],environment_variables=[],features={'file_upload':{'enabled':False},'retriever_resource':{'enabled':False},'sensitive_word_avoidance':{'enabled':False},'speech_to_text':{'enabled':False},'text_to_speech':{'enabled':False},'suggested_questions_after_answer':{'enabled':False}},graph=dict(nodes=nodes,edges=edges,viewport=dict(x=0,y=0,zoom=.7)))) nodes=[start(),http('prepare','读取聊天与已有故事','prepare',360,80,kind='extract',prepared=False),parse('prepared','接收任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('classify','DeepSeek:分类与故事拆分','classify',670,300,prompt=extract_prompt),parse('classified','检查故事拆分结果','classify',{'story_count':'number','daily_candidate_count':'number'},360,300),http('daily','DeepSeek:只审核今天实际事件','daily',50,300,prompt=daily_prompt),parse('today','检查当日原话证据','daily',{'today_actual_count':'number'},50,520),http('dedup','去重:同事件比较新增信息','dedup',360,520),parse('merged','输出新增 / 更新 / 重复计划','dedup',{'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'},670,520)] fields={'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'};nodes.append(end('merged',fields,670,740));edges=[edge(a['id'],b['id'],nodes) for a,b in zip(nodes,nodes[1:])] extract=app('聊天整理:当日日常与回忆故事','DeepSeek 分类与事件拆分 → 独立审核今天已发生事项 → 同故事去重与新增信息比较。只记录任务编号和统计;原话、权限检查和故事更新由故事平台负责。',nodes,edges) (DEST/'chat-to-stories.yml').write_text(json.dumps(extract,ensure_ascii=False,indent=2),encoding='utf-8') nodes=[start(),http('prepare','读取长者全部可用故事','prepare',360,80,kind='memoir',prepared=False),parse('prepared','接收整本任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('write-book','Doubao:全部故事统一编写整本','write-book',670,300,prompt=book_prompt),parse('written','检查完整章节与插画计划','write-book',{'chapter_count':'number','chapter_indexes':'array[number]'},360,300),http('validate-book','核验来源与日期 · 先显示正文','validate-book',50,300),parse('validated','所有故事均已成章','validate-book',{'chapter_count':'number'},50,520)] next(n for n in nodes if n['id']=='write-book')['data']['timeout']['read']=660 iteration=node('pictures','iteration','逐章配图 · 并行 3 章',360,520,iterator_selector=['written','chapter_indexes'],iterator_input_type='array[number]',output_selector=['picture-result','result'],output_type='array[string]',is_parallel=True,parallel_nums=3,error_handle_mode='terminated',start_node_id='pictures-start',width=630,height=235) iteration['width']=630;iteration['height']=235;iteration['zIndex']=1;nodes.append(iteration) inner_start=node('pictures-start','iteration-start','',24,80,isInIteration=True);inner_start.update(type='custom-iteration-start',parentId='pictures',width=44,height=48,draggable=False,selectable=False,zIndex=1002) image=http('illustrate','Seedream:绘制该章情景插画','illustrate',95,65,index=True);image.update(parentId='pictures',extent='parent',zIndex=1002);image['data'].update(isInIteration=True,isInLoop=False,iteration_id='pictures') image_result=node('picture-result','code','保留正文,记录插画结果',365,65,code_language='python3',code='import json\ndef main(body: str) -> dict:\n data=json.loads(body)\n return {"result":json.dumps(data,ensure_ascii=False)}\n',variables=[{'variable':'body','value_selector':['illustrate','body']}],outputs={'result':{'type':'string','children':None}},isInIteration=True,isInLoop=False,iteration_id='pictures');image_result.update(parentId='pictures',extent='parent',zIndex=1002) nodes += [inner_start,image,image_result,http('finish','汇总正文与插画完成情况','finish',50,850),parse('finished','结果核验与图片数量','finish',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},360,850),end('finished',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},670,850)] main=['start','prepare','prepared','write-book','written','validate-book','validated','pictures','finish','finished','end'];edges=[edge(a,b,nodes) for a,b in zip(main,main[1:])]+[edge('pictures-start','illustrate',nodes,'pictures'),edge('illustrate','picture-result',nodes,'pictures')] book=app('图文回忆录:全部故事成书与逐章配图','全部故事一次交给 Doubao-Seed-Evolving 统一成书 → 核验章节、事实与日期 → 正文先显示 → Seedream 并行配图 → 后台检查账号授权和来源版本并保存。',nodes,edges) (DEST/'stories-to-memoir.yml').write_text(json.dumps(book,ensure_ascii=False,indent=2),encoding='utf-8') if args.provider_id: spec = importlib.util.spec_from_file_location('dify_model_tools', ROOT/'scripts/dify-model-tools.py') tools = importlib.util.module_from_spec(spec) spec.loader.exec_module(tools) for workflow, filename in [(extract, 'chat-to-stories.yml'), (book, 'stories-to-memoir.yml')]: workflow['workflow']['graph'] = tools.convert_graph(workflow['workflow']['graph'], args.provider_id) (DEST/filename).write_text(json.dumps(workflow,ensure_ascii=False,indent=2),encoding='utf-8') (DEST.parent/'model-tools.openapi.json').write_text(json.dumps(tools.schema(),ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps({'workflows':2,'nodes':[len(extract['workflow']['graph']['nodes']),len(book['workflow']['graph']['nodes'])]}))