4949 add_memory as _add_memory ,
5050)
5151from open_webui .routers .retrieval import search_web as _search_web
52+ from open_webui .events import EVENTS , publish_event
53+ from open_webui .socket .main import sio
5254from open_webui .utils .sanitize import sanitize_code
5355
5456log = logging .getLogger (__name__ )
5557
5658MAX_KNOWLEDGE_BASE_SEARCH_ITEMS = 10_000
5759
5860
61+ async def _has_write_access_to_note (note , user_id : str ) -> bool :
62+ if note .user_id == user_id :
63+ return True
64+
65+ from open_webui .models .access_grants import AccessGrants
66+
67+ user_group_ids = [group .id for group in await Groups .get_groups_by_member_id (user_id )]
68+ return await AccessGrants .has_access (
69+ user_id = user_id ,
70+ resource_type = 'note' ,
71+ resource_id = note .id ,
72+ permission = 'write' ,
73+ user_group_ids = set (user_group_ids ),
74+ )
75+
76+
77+ async def _emit_note_updated (request : Request , user : dict , note ) -> None :
78+ await sio .emit ('note-events' , note .model_dump (), to = f'note:{ note .id } ' )
79+ await publish_event (
80+ request ,
81+ EVENTS .NOTE_UPDATED ,
82+ actor = user ,
83+ subject_id = note .id ,
84+ data = {'title' : note .title },
85+ )
86+
87+
5988async def _has_read_access_to_file (
6089 file ,
6190 user_id : str ,
@@ -1155,23 +1184,21 @@ async def replace_note_content(
11551184 if not note :
11561185 return json .dumps ({'error' : 'Note not found' })
11571186
1158- # Check write permission
11591187 user_id = __user__ .get ('id' )
1160- user_group_ids = [group .id for group in await Groups .get_groups_by_member_id (user_id )]
1161-
1162- from open_webui .models .access_grants import AccessGrants
1163-
1164- if note .user_id != user_id and not await AccessGrants .has_access (
1165- user_id = user_id ,
1166- resource_type = 'note' ,
1167- resource_id = note .id ,
1168- permission = 'write' ,
1169- user_group_ids = set (user_group_ids ),
1170- ):
1188+ if not await _has_write_access_to_note (note , user_id ):
11711189 return json .dumps ({'error' : 'Write access denied' })
11721190
1173- # Build update form
1174- update_data = {'data' : {'content' : {'md' : content }}}
1191+ update_data = {
1192+ 'data' : {
1193+ ** (note .data or {}),
1194+ 'content' : {
1195+ ** ((note .data or {}).get ('content' ) or {}),
1196+ 'json' : None ,
1197+ 'html' : '' ,
1198+ 'md' : content ,
1199+ },
1200+ }
1201+ }
11751202 if title :
11761203 update_data ['title' ] = title
11771204
@@ -1181,6 +1208,8 @@ async def replace_note_content(
11811208 if not updated_note :
11821209 return json .dumps ({'error' : 'Failed to update note' })
11831210
1211+ await _emit_note_updated (__request__ , __user__ , updated_note )
1212+
11841213 return json .dumps (
11851214 {
11861215 'status' : 'success' ,
@@ -1195,6 +1224,88 @@ async def replace_note_content(
11951224 return json .dumps ({'error' : str (e )})
11961225
11971226
1227+ async def replace_note_text (
1228+ note_id : str ,
1229+ target_text : str ,
1230+ replacement_text : str ,
1231+ title : Optional [str ] = None ,
1232+ __request__ : Request = None ,
1233+ __user__ : dict = None ,
1234+ ) -> str :
1235+ """
1236+ Replace an exact text span in a note when it occurs exactly once.
1237+
1238+ :param note_id: The ID of the note to update
1239+ :param target_text: Exact text to replace
1240+ :param replacement_text: Replacement text
1241+ :param title: Optional new title for the note
1242+ :return: JSON with success status or a clear match-count error
1243+ """
1244+ if __request__ is None :
1245+ return json .dumps ({'error' : 'Request context not available' })
1246+
1247+ if not __user__ :
1248+ return json .dumps ({'error' : 'User context not available' })
1249+
1250+ try :
1251+ from open_webui .models .notes import NoteUpdateForm
1252+
1253+ note = await Notes .get_note_by_id (note_id )
1254+ if not note :
1255+ return json .dumps ({'error' : 'Note not found' })
1256+
1257+ user_id = __user__ .get ('id' )
1258+ if not await _has_write_access_to_note (note , user_id ):
1259+ return json .dumps ({'error' : 'Write access denied' })
1260+
1261+ if target_text == '' :
1262+ return json .dumps ({'error' : 'target_text must not be empty' })
1263+
1264+ content = ((note .data or {}).get ('content' ) or {}).get ('md' ) or ''
1265+ match_count = content .count (target_text )
1266+ if match_count != 1 :
1267+ return json .dumps (
1268+ {
1269+ 'error' : 'target_text must occur exactly once' ,
1270+ 'match_count' : match_count ,
1271+ },
1272+ ensure_ascii = False ,
1273+ )
1274+
1275+ update_data = {
1276+ 'data' : {
1277+ ** (note .data or {}),
1278+ 'content' : {
1279+ ** ((note .data or {}).get ('content' ) or {}),
1280+ 'json' : None ,
1281+ 'html' : '' ,
1282+ 'md' : content .replace (target_text , replacement_text , 1 ),
1283+ },
1284+ }
1285+ }
1286+ if title :
1287+ update_data ['title' ] = title
1288+
1289+ updated_note = await Notes .update_note_by_id (note_id , NoteUpdateForm (** update_data ))
1290+ if not updated_note :
1291+ return json .dumps ({'error' : 'Failed to update note' })
1292+
1293+ await _emit_note_updated (__request__ , __user__ , updated_note )
1294+
1295+ return json .dumps (
1296+ {
1297+ 'status' : 'success' ,
1298+ 'id' : updated_note .id ,
1299+ 'title' : updated_note .title ,
1300+ 'updated_at' : updated_note .updated_at ,
1301+ },
1302+ ensure_ascii = False ,
1303+ )
1304+ except Exception as e :
1305+ log .exception (f'replace_note_text error: { e } ' )
1306+ return json .dumps ({'error' : str (e )})
1307+
1308+
11981309# =============================================================================
11991310# CHATS TOOLS
12001311# =============================================================================
0 commit comments